diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt index c6d9afcf6ab9..641e36d06643 100644 --- a/lib/galaxy/dependencies/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pinned-requirements.txt @@ -64,7 +64,6 @@ ecdsa==0.18.0 ; python_version >= "3.8" and python_version < "3.12" edam-ontology==1.25.2 ; python_version >= "3.8" and python_version < "3.12" email-validator==2.1.0.post1 ; python_version >= "3.8" and python_version < "3.12" exceptiongroup==1.2.0 ; python_version >= "3.8" and python_version < "3.11" -fastapi-utils==0.2.1 ; python_version >= "3.8" and python_version < "3.12" fastapi==0.98.0 ; python_version >= "3.8" and python_version < "3.12" filelock==3.13.1 ; python_version >= "3.8" and python_version < "3.12" frozenlist==1.4.1 ; python_version >= "3.8" and python_version < "3.12" diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index 6b633463cd21..f39c570ddee6 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -38,7 +38,6 @@ APIKeyHeader, APIKeyQuery, ) -from fastapi_utils.cbv import cbv from pydantic import ValidationError from pydantic.main import BaseModel from starlette.datastructures import Headers @@ -442,15 +441,6 @@ def _handle_galaxy_kwd(self, kwd): return kwd - @property - def cbv(self): - """Short-hand for frequently used Galaxy-pattern of FastAPI class based views. - - Creates a class-based view for for this router, for more information see: - https://fastapi-utils.davidmontague.xyz/user-guide/class-based-views/ - """ - return cbv(self) - class Router(FrameworkRouter): admin_user_dependency = AdminUserRequired diff --git a/lib/galaxy/webapps/galaxy/api/authenticate.py b/lib/galaxy/webapps/galaxy/api/authenticate.py index 8c5a7d5b780e..603232ff603b 100644 --- a/lib/galaxy/webapps/galaxy/api/authenticate.py +++ b/lib/galaxy/webapps/galaxy/api/authenticate.py @@ -50,15 +50,13 @@ def options(self, trans: GalaxyWebTransaction, **kwd): # trans.response.headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS, DELETE' -@router.cbv -class FastAPIAuthenticate: - authentication_service: AuthenticationService = depends(AuthenticationService) - - @router.get( - "/api/authenticate/baseauth", - summary="Returns returns an API key for authenticated user based on BaseAuth headers.", - ) - def get_api_key(self, request: Request) -> APIKeyResponse: - authorization = request.headers.get("Authorization") - auth = {"HTTP_AUTHORIZATION": authorization} - return self.authentication_service.get_api_key(auth, request) +@router.get( + "/api/authenticate/baseauth", + summary="Returns returns an API key for authenticated user based on BaseAuth headers.", +) +def get_api_key( + request: Request, authentication_service: AuthenticationService = depends(AuthenticationService) +) -> APIKeyResponse: + authorization = request.headers.get("Authorization") + auth = {"HTTP_AUTHORIZATION": authorization} + return authentication_service.get_api_key(auth, request) diff --git a/lib/galaxy/webapps/galaxy/api/cloud.py b/lib/galaxy/webapps/galaxy/api/cloud.py index 7bb61c2ec7b9..8b38322b752a 100644 --- a/lib/galaxy/webapps/galaxy/api/cloud.py +++ b/lib/galaxy/webapps/galaxy/api/cloud.py @@ -32,72 +32,69 @@ router = Router(tags=["cloud"]) -@router.cbv -class FastAPICloudController: - cloud_manager: CloudManager = depends(CloudManager) - datasets_serializer: DatasetSerializer = depends(DatasetSerializer) +@router.get( + "/api/cloud/storage", + summary="Lists cloud-based buckets (e.g., S3 bucket, Azure blob) user has defined. Is not yet implemented", + deprecated=True, +) +def index( + response: Response, +): + # TODO: This can be implemented leveraging PluggedMedia objects (part of the user-based object store project) + response.status_code = status.HTTP_501_NOT_IMPLEMENTED + return StatusCode(detail="Not yet implemented.", status=501) - @router.get( - "/api/cloud/storage", - summary="Lists cloud-based buckets (e.g., S3 bucket, Azure blob) user has defined. Is not yet implemented", - deprecated=True, - ) - def index( - self, - response: Response, - ): - # TODO: This can be implemented leveraging PluggedMedia objects (part of the user-based object store project) - response.status_code = status.HTTP_501_NOT_IMPLEMENTED - return StatusCode(detail="Not yet implemented.", status=501) - @router.post( - "/api/cloud/storage/get", - summary="Gets given objects from a given cloud-based bucket to a Galaxy history.", - deprecated=True, +@router.post( + "/api/cloud/storage/get", + summary="Gets given objects from a given cloud-based bucket to a Galaxy history.", + deprecated=True, +) +def get( + payload: CloudObjects = Body(default=Required), + trans: ProvidesHistoryContext = DependsOnTrans, + cloud_manager: CloudManager = depends(CloudManager), + datasets_serializer: DatasetSerializer = depends(DatasetSerializer), +) -> DatasetSummaryList: + datasets = cloud_manager.get( + trans=trans, + history_id=payload.history_id, + bucket_name=payload.bucket, + objects=payload.objects, + authz_id=payload.authz_id, + input_args=payload.input_args, ) - def get( - self, - payload: CloudObjects = Body(default=Required), - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> DatasetSummaryList: - datasets = self.cloud_manager.get( - trans=trans, - history_id=payload.history_id, - bucket_name=payload.bucket, - objects=payload.objects, - authz_id=payload.authz_id, - input_args=payload.input_args, - ) - rtv = [] - for dataset in datasets: - rtv.append(self.datasets_serializer.serialize_to_view(dataset, view="summary")) - return DatasetSummaryList.construct(__root__=rtv) + rtv = [] + for dataset in datasets: + rtv.append(datasets_serializer.serialize_to_view(dataset, view="summary")) + return DatasetSummaryList.construct(__root__=rtv) - @router.post( - "/api/cloud/storage/send", - summary="Sends given dataset(s) in a given history to a given cloud-based bucket.", - deprecated=True, - ) - def send( - self, - payload: CloudDatasets = Body(default=Required), - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> CloudDatasetsResponse: - log.info( - msg="Received api/send request for `{}` datasets using authnz with id `{}`, and history `{}`." - "".format( - "all the dataset in the given history" if not payload.dataset_ids else len(payload.dataset_ids), - payload.authz_id, - payload.history_id, - ) - ) - sent, failed = self.cloud_manager.send( - trans=trans, - history_id=payload.history_id, - bucket_name=payload.bucket, - authz_id=payload.authz_id, - dataset_ids=payload.dataset_ids, - overwrite_existing=payload.overwrite_existing, +@router.post( + "/api/cloud/storage/send", + summary="Sends given dataset(s) in a given history to a given cloud-based bucket.", + deprecated=True, +) +def send( + payload: CloudDatasets = Body(default=Required), + trans: ProvidesHistoryContext = DependsOnTrans, + cloud_manager: CloudManager = depends(CloudManager), +) -> CloudDatasetsResponse: + log.info( + msg="Received api/send request for `{}` datasets using authnz with id `{}`, and history `{}`." + "".format( + "all the dataset in the given history" if not payload.dataset_ids else len(payload.dataset_ids), + payload.authz_id, + payload.history_id, ) - return CloudDatasetsResponse(sent_dataset_labels=sent, failed_dataset_labels=failed, bucket_name=payload.bucket) + ) + + sent, failed = cloud_manager.send( + trans=trans, + history_id=payload.history_id, + bucket_name=payload.bucket, + authz_id=payload.authz_id, + dataset_ids=payload.dataset_ids, + overwrite_existing=payload.overwrite_existing, + ) + return CloudDatasetsResponse(sent_dataset_labels=sent, failed_dataset_labels=failed, bucket_name=payload.bucket) diff --git a/lib/galaxy/webapps/galaxy/api/configuration.py b/lib/galaxy/webapps/galaxy/api/configuration.py index 6ad12cbc1dd1..7082d5c5d774 100644 --- a/lib/galaxy/webapps/galaxy/api/configuration.py +++ b/lib/galaxy/webapps/galaxy/api/configuration.py @@ -39,84 +39,97 @@ ) -@router.cbv -class FastAPIConfiguration: - configuration_manager: ConfigurationManager = depends(ConfigurationManager) - - @router.get( - "/api/whoami", - summary="Return information about the current authenticated user", - response_description="Information about the current authenticated user", - ) - def whoami(self, trans: ProvidesUserContext = DependsOnTrans) -> Optional[UserModel]: - """Return information about the current authenticated user.""" - return _user_to_model(trans.user) - - @router.get( - "/api/configuration", - summary="Return an object containing exposable configuration settings", - response_description="Object containing exposable configuration settings", - ) - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - view: Optional[str] = SerializationViewQueryParam, - keys: Optional[str] = SerializationKeysQueryParam, - ) -> Dict[str, Any]: - """ - Return an object containing exposable configuration settings. - - A more complete list is returned if the user is an admin. - Pass in `view` and a comma-seperated list of keys to control which - configuration settings are returned. - """ - return _index(self.configuration_manager, trans, view, keys) - - @router.get( - "/api/version", - summary="Return Galaxy version information: major/minor version, optional extra info", - response_description="Galaxy version information: major/minor version, optional extra info", - ) - def version(self) -> Dict[str, Any]: - """Return Galaxy version information: major/minor version, optional extra info.""" - return self.configuration_manager.version() - - @router.get( - "/api/configuration/dynamic_tool_confs", - require_admin=True, - summary="Return dynamic tool configuration files", - response_description="Dynamic tool configuration files", - ) - def dynamic_tool_confs(self) -> List[Dict[str, str]]: - """Return dynamic tool configuration files.""" - return self.configuration_manager.dynamic_tool_confs() - - @router.get( - "/api/configuration/decode/{encoded_id}", - require_admin=True, - summary="Decode a given id", - response_description="Decoded id", - ) - def decode_id(self, encoded_id: str = EncodedIdPathParam) -> Dict[str, int]: - """Decode a given id.""" - return self.configuration_manager.decode_id(encoded_id) - - @router.get( - "/api/configuration/tool_lineages", - require_admin=True, - summary="Return tool lineages for tools that have them", - response_description="Tool lineages for tools that have them", - ) - def tool_lineages(self) -> List[Dict[str, Dict]]: - """Return tool lineages for tools that have them.""" - return self.configuration_manager.tool_lineages() - - @router.put( - "/api/configuration/toolbox", require_admin=True, summary="Reload the Galaxy toolbox (but not individual tools)" - ) - def reload_toolbox(self): - """Reload the Galaxy toolbox (but not individual tools).""" - self.configuration_manager.reload_toolbox() +@router.get( + "/api/whoami", + summary="Return information about the current authenticated user", + response_description="Information about the current authenticated user", +) +def whoami(trans: ProvidesUserContext = DependsOnTrans) -> Optional[UserModel]: + """Return information about the current authenticated user.""" + return _user_to_model(trans.user) + + +@router.get( + "/api/configuration", + summary="Return an object containing exposable configuration settings", + response_description="Object containing exposable configuration settings", +) +def index( + trans: ProvidesUserContext = DependsOnTrans, + view: Optional[str] = SerializationViewQueryParam, + keys: Optional[str] = SerializationKeysQueryParam, + configuration_manager: ConfigurationManager = depends(ConfigurationManager), +) -> Dict[str, Any]: + """ + Return an object containing exposable configuration settings. + + A more complete list is returned if the user is an admin. + Pass in `view` and a comma-seperated list of keys to control which + configuration settings are returned. + """ + return _index(configuration_manager, trans, view, keys) + + +@router.get( + "/api/version", + summary="Return Galaxy version information: major/minor version, optional extra info", + response_description="Galaxy version information: major/minor version, optional extra info", +) +def version( + configuration_manager: ConfigurationManager = depends(ConfigurationManager), +) -> Dict[str, Any]: + """Return Galaxy version information: major/minor version, optional extra info.""" + return configuration_manager.version() + + +@router.get( + "/api/configuration/dynamic_tool_confs", + require_admin=True, + summary="Return dynamic tool configuration files", + response_description="Dynamic tool configuration files", +) +def dynamic_tool_confs( + configuration_manager: ConfigurationManager = depends(ConfigurationManager), +) -> List[Dict[str, str]]: + """Return dynamic tool configuration files.""" + return configuration_manager.dynamic_tool_confs() + + +@router.get( + "/api/configuration/decode/{encoded_id}", + require_admin=True, + summary="Decode a given id", + response_description="Decoded id", +) +def decode_id( + encoded_id: str = EncodedIdPathParam, + configuration_manager: ConfigurationManager = depends(ConfigurationManager), +) -> Dict[str, int]: + """Decode a given id.""" + return configuration_manager.decode_id(encoded_id) + + +@router.get( + "/api/configuration/tool_lineages", + require_admin=True, + summary="Return tool lineages for tools that have them", + response_description="Tool lineages for tools that have them", +) +def tool_lineages( + configuration_manager: ConfigurationManager = depends(ConfigurationManager), +) -> List[Dict[str, Dict]]: + """Return tool lineages for tools that have them.""" + return configuration_manager.tool_lineages() + + +@router.put( + "/api/configuration/toolbox", require_admin=True, summary="Reload the Galaxy toolbox (but not individual tools)" +) +def reload_toolbox( + configuration_manager: ConfigurationManager = depends(ConfigurationManager), +): + """Reload the Galaxy toolbox (but not individual tools).""" + configuration_manager.reload_toolbox() def _user_to_model(user): diff --git a/lib/galaxy/webapps/galaxy/api/dataset_collections.py b/lib/galaxy/webapps/galaxy/api/dataset_collections.py index 8f164c4b122d..647f468d28ad 100644 --- a/lib/galaxy/webapps/galaxy/api/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/api/dataset_collections.py @@ -47,98 +47,100 @@ ) -@router.cbv -class FastAPIDatasetCollections: - service: DatasetCollectionsService = depends(DatasetCollectionsService) - - @router.post( - "/api/dataset_collections", - summary="Create a new dataset collection instance.", - ) - def create( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - payload: CreateNewCollectionPayload = Body(...), - ) -> HDCADetailed: - return self.service.create(trans, payload) - - @router.post( - "/api/dataset_collections/{id}/copy", - summary="Copy the given collection datasets to a new collection using a new `dbkey` attribute.", - ) - def copy( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - id: DecodedDatabaseIdField = Path(..., description="The ID of the dataset collection to copy."), - payload: UpdateCollectionAttributePayload = Body(...), - ): - self.service.copy(trans, id, payload) - - @router.get( - "/api/dataset_collections/{id}/attributes", - summary="Returns `dbkey`/`extension` attributes for all the collection elements.", - ) - def attributes( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, - instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, - ) -> DatasetCollectionAttributesResult: - return self.service.attributes(trans, id, instance_type) - - @router.get( - "/api/dataset_collections/{id}/suitable_converters", - summary="Returns a list of applicable converters for all datatypes in the given collection.", - ) - def suitable_converters( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, - instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, - ) -> SuitableConverters: - return self.service.suitable_converters(trans, id, instance_type) - - @router.get( - "/api/dataset_collections/{id}", - summary="Returns detailed information about the given collection.", - ) - def show( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, - instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, - ) -> HDCADetailed: - return self.service.show(trans, id, instance_type) - - @router.get( - "/api/dataset_collections/{hdca_id}/contents/{parent_id}", - name="contents_dataset_collection", - summary="Returns direct child contents of indicated dataset collection parent ID.", - ) - def contents( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - hdca_id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, - parent_id: DecodedDatabaseIdField = Path( - ..., - description="Parent collection ID describing what collection the contents belongs to.", - ), - instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, - limit: Optional[int] = Query( - default=None, - description="The maximum number of content elements to return.", - ), - offset: Optional[int] = Query( - default=None, - description="The number of content elements that will be skipped before returning.", - ), - ) -> DatasetCollectionContentElements: - return self.service.contents(trans, hdca_id, parent_id, instance_type, limit, offset) - - @router.get("/api/dataset_collection_element/{dce_id}") - def content( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - dce_id: DecodedDatabaseIdField = DatasetCollectionElementIdPathParam, - ) -> DCESummary: - return self.service.dce_content(trans, dce_id) +@router.post( + "/api/dataset_collections", + summary="Create a new dataset collection instance.", +) +def create( + trans: ProvidesHistoryContext = DependsOnTrans, + payload: CreateNewCollectionPayload = Body(...), + service: DatasetCollectionsService = depends(DatasetCollectionsService), +) -> HDCADetailed: + return service.create(trans, payload) + + +@router.post( + "/api/dataset_collections/{id}/copy", + summary="Copy the given collection datasets to a new collection using a new `dbkey` attribute.", +) +def copy( + trans: ProvidesHistoryContext = DependsOnTrans, + id: DecodedDatabaseIdField = Path(..., description="The ID of the dataset collection to copy."), + payload: UpdateCollectionAttributePayload = Body(...), + service: DatasetCollectionsService = depends(DatasetCollectionsService), +): + service.copy(trans, id, payload) + + +@router.get( + "/api/dataset_collections/{id}/attributes", + summary="Returns `dbkey`/`extension` attributes for all the collection elements.", +) +def attributes( + trans: ProvidesHistoryContext = DependsOnTrans, + id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, + instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, + service: DatasetCollectionsService = depends(DatasetCollectionsService), +) -> DatasetCollectionAttributesResult: + return service.attributes(trans, id, instance_type) + + +@router.get( + "/api/dataset_collections/{id}/suitable_converters", + summary="Returns a list of applicable converters for all datatypes in the given collection.", +) +def suitable_converters( + trans: ProvidesHistoryContext = DependsOnTrans, + id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, + instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, + service: DatasetCollectionsService = depends(DatasetCollectionsService), +) -> SuitableConverters: + return service.suitable_converters(trans, id, instance_type) + + +@router.get( + "/api/dataset_collections/{id}", + summary="Returns detailed information about the given collection.", +) +def show( + trans: ProvidesHistoryContext = DependsOnTrans, + id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, + instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, + service: DatasetCollectionsService = depends(DatasetCollectionsService), +) -> HDCADetailed: + return service.show(trans, id, instance_type) + + +@router.get( + "/api/dataset_collections/{hdca_id}/contents/{parent_id}", + name="contents_dataset_collection", + summary="Returns direct child contents of indicated dataset collection parent ID.", +) +def contents( + trans: ProvidesHistoryContext = DependsOnTrans, + hdca_id: DecodedDatabaseIdField = DatasetCollectionIdPathParam, + parent_id: DecodedDatabaseIdField = Path( + ..., + description="Parent collection ID describing what collection the contents belongs to.", + ), + instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam, + limit: Optional[int] = Query( + default=None, + description="The maximum number of content elements to return.", + ), + offset: Optional[int] = Query( + default=None, + description="The number of content elements that will be skipped before returning.", + ), + service: DatasetCollectionsService = depends(DatasetCollectionsService), +) -> DatasetCollectionContentElements: + return service.contents(trans, hdca_id, parent_id, instance_type, limit, offset) + + +@router.get("/api/dataset_collection_element/{dce_id}") +def content( + trans: ProvidesHistoryContext = DependsOnTrans, + dce_id: DecodedDatabaseIdField = DatasetCollectionElementIdPathParam, + service: DatasetCollectionsService = depends(DatasetCollectionsService), +) -> DCESummary: + return service.dce_content(trans, dce_id) diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index 14d4bc1fe019..0a541a561dee 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -129,355 +129,369 @@ ) -@router.cbv -class FastAPIDatasets: - service: DatasetsService = depends(DatasetsService) +@router.get( + "/api/datasets", + summary="Search datasets or collections using a query system.", +) +def index( + trans=DependsOnTrans, + history_id: Optional[DecodedDatabaseIdField] = Query( + default=None, + description="Optional identifier of a History. Use it to restrict the search within a particular History.", + ), + serialization_params: SerializationParams = Depends(query_serialization_params), + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + service: DatasetsService = depends(DatasetsService), +) -> List[AnyHistoryContentItem]: + return service.index(trans, history_id, serialization_params, filter_query_params) - @router.get( - "/api/datasets", - summary="Search datasets or collections using a query system.", - ) - def index( - self, - trans=DependsOnTrans, - history_id: Optional[DecodedDatabaseIdField] = Query( - default=None, - description="Optional identifier of a History. Use it to restrict the search within a particular History.", - ), - serialization_params: SerializationParams = Depends(query_serialization_params), - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - ) -> List[AnyHistoryContentItem]: - return self.service.index(trans, history_id, serialization_params, filter_query_params) - - @router.get( - "/api/datasets/{dataset_id}/storage", - summary="Display user-facing storage details related to the objectstore a dataset resides in.", - ) - def show_storage( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - hda_ldda: DatasetSourceType = DatasetSourceQueryParam, - ) -> DatasetStorageDetails: - return self.service.show_storage(trans, dataset_id, hda_ldda) - - @router.get( - "/api/datasets/{dataset_id}/inheritance_chain", - summary="For internal use, this endpoint may change without warning.", - include_in_schema=True, # Can be changed to False if we don't really want to expose this - ) - def show_inheritance_chain( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - hda_ldda: DatasetSourceType = DatasetSourceQueryParam, - ) -> DatasetInheritanceChain: - return self.service.show_inheritance_chain(trans, dataset_id, hda_ldda) - - @router.get( - "/api/datasets/{dataset_id}/get_content_as_text", - summary="Returns dataset content as Text.", - ) - def get_content_as_text( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - ) -> DatasetTextContentDetails: - return self.service.get_content_as_text(trans, dataset_id) - - @router.get( - "/api/datasets/{dataset_id}/converted/{ext}", - summary="Return information about datasets made by converting this dataset to a new format.", - ) - def converted_ext( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - ext: str = Path( - ..., - description="File extension of the new format to convert this dataset to.", - ), - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHDA: - """ - Return information about datasets made by converting this dataset to a new format. - If there is no existing converted dataset for the format in `ext`, one will be created. +@router.get( + "/api/datasets/{dataset_id}/storage", + summary="Display user-facing storage details related to the objectstore a dataset resides in.", +) +def show_storage( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + hda_ldda: DatasetSourceType = DatasetSourceQueryParam, + service: DatasetsService = depends(DatasetsService), +) -> DatasetStorageDetails: + return service.show_storage(trans, dataset_id, hda_ldda) + + +@router.get( + "/api/datasets/{dataset_id}/inheritance_chain", + summary="For internal use, this endpoint may change without warning.", + include_in_schema=True, # Can be changed to False if we don't really want to expose this +) +def show_inheritance_chain( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + hda_ldda: DatasetSourceType = DatasetSourceQueryParam, + service: DatasetsService = depends(DatasetsService), +) -> DatasetInheritanceChain: + return service.show_inheritance_chain(trans, dataset_id, hda_ldda) + + +@router.get( + "/api/datasets/{dataset_id}/get_content_as_text", + summary="Returns dataset content as Text.", +) +def get_content_as_text( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + service: DatasetsService = depends(DatasetsService), +) -> DatasetTextContentDetails: + return service.get_content_as_text(trans, dataset_id) - **Note**: `view` and `keys` are also available to control the serialization of the dataset. - """ - return self.service.converted_ext(trans, dataset_id, ext, serialization_params) - @router.get( - "/api/datasets/{dataset_id}/converted", - summary=("Return a a map with all the existing converted datasets associated with this instance."), - ) - def converted( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - ) -> ConvertedDatasetsMap: - """ - Return a map of ` : ` containing all the *existing* converted datasets. - """ - return self.service.converted(trans, dataset_id) - - @router.put( - "/api/datasets/{dataset_id}/permissions", - summary="Set permissions of the given history dataset to the given role ids.", - ) - def update_permissions( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - # Using a generic Dict here as an attempt on supporting multiple aliases for the permissions params. - payload: Dict[str, Any] = Body( - default=..., - example=UpdateDatasetPermissionsPayload(), - ), - ) -> DatasetAssociationRoles: - """Set permissions of the given history dataset to the given role ids.""" - update_payload = get_update_permission_payload(payload) - return self.service.update_permissions(trans, dataset_id, update_payload) - - @router.get( - "/api/histories/{history_id}/contents/{history_content_id}/extra_files", - summary="Get the list of extra files/directories associated with a dataset.", - tags=["histories"], - ) - def extra_files_history( - self, - trans=DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, - ) -> DatasetExtraFiles: - return self.service.extra_files(trans, history_content_id) - - @router.get( - "/api/datasets/{dataset_id}/extra_files", - summary="Get the list of extra files/directories associated with a dataset.", - ) - def extra_files( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - ) -> DatasetExtraFiles: - return self.service.extra_files(trans, dataset_id) - - @router.get( - "/api/histories/{history_id}/contents/{history_content_id}/display", - name="history_contents_display", - summary="Displays (preview) or downloads dataset content.", - tags=["histories"], - response_class=StreamingResponse, - ) - @router.head( - "/api/histories/{history_id}/contents/{history_content_id}/display", - name="history_contents_display", - summary="Check if dataset content can be previewed or downloaded.", - tags=["histories"], - ) - def display_history_content( - self, - request: Request, - trans=DependsOnTrans, - history_id: Optional[DecodedDatabaseIdField] = HistoryIDPathParam, - history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, - preview: bool = PreviewQueryParam, - filename: Optional[str] = FilenameQueryParam, - to_ext: Optional[str] = ToExtQueryParam, - raw: bool = RawQueryParam, - offset: Optional[int] = DisplayOffsetQueryParam, - ck_size: Optional[int] = DisplayChunkSizeQueryParam, - ): - """Streams the dataset for download or the contents preview to be displayed in a browser.""" - return self._display(request, trans, history_content_id, preview, filename, to_ext, raw, offset, ck_size) - - @router.get( - "/api/datasets/{history_content_id}/display", - summary="Displays (preview) or downloads dataset content.", - response_class=StreamingResponse, - ) - @router.head( - "/api/datasets/{history_content_id}/display", - summary="Check if dataset content can be previewed or downloaded.", - ) - def display( - self, - request: Request, - trans=DependsOnTrans, - history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, - preview: bool = PreviewQueryParam, - filename: Optional[str] = FilenameQueryParam, - to_ext: Optional[str] = ToExtQueryParam, - raw: bool = RawQueryParam, - offset: Optional[int] = DisplayOffsetQueryParam, - ck_size: Optional[int] = DisplayChunkSizeQueryParam, - ): - """Streams the dataset for download or the contents preview to be displayed in a browser.""" - return self._display(request, trans, history_content_id, preview, filename, to_ext, raw, offset, ck_size) - - def _display( - self, - request: Request, - trans, - history_content_id: DecodedDatabaseIdField, - preview: bool, - filename: Optional[str], - to_ext: Optional[str], - raw: bool, - offset: Optional[int] = None, - ck_size: Optional[int] = None, - ): - extra_params = get_query_parameters_from_request_excluding( - request, {"preview", "filename", "to_ext", "raw", "dataset", "ck_size", "offset"} - ) - display_data, headers = self.service.display( - trans, - history_content_id, - preview=preview, - filename=filename, - to_ext=to_ext, - raw=raw, - offset=offset, - ck_size=ck_size, - **extra_params, - ) - if isinstance(display_data, IOBase): - file_name = getattr(display_data, "name", None) - if file_name: - return GalaxyFileResponse(file_name, headers=headers, method=request.method) - elif isinstance(display_data, ZipstreamWrapper): - return StreamingResponse(display_data.response(), headers=headers) - elif isinstance(display_data, bytes): - return StreamingResponse(BytesIO(display_data), headers=headers) - elif isinstance(display_data, str): - return StreamingResponse(content=StringIO(display_data), headers=headers) - return StreamingResponse(display_data, headers=headers) - - @router.get( - "/api/histories/{history_id}/contents/{history_content_id}/metadata_file", - summary="Returns the metadata file associated with this history item.", - name="get_metadata_file", - tags=["histories"], - operation_id="history_contents__get_metadata_file", - response_class=GalaxyFileResponse, - ) - def get_metadata_file_history_content( - self, - trans=DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, - metadata_file: str = Query( - ..., - description="The name of the metadata file to retrieve.", - ), - ): - return self._get_metadata_file(trans, history_content_id, metadata_file) - - @router.get( - "/api/datasets/{history_content_id}/metadata_file", - summary="Returns the metadata file associated with this history item.", - response_class=GalaxyFileResponse, - operation_id="datasets__get_metadata_file", - ) - @router.head( - "/api/datasets/{history_content_id}/metadata_file", - summary="Check if metadata file can be downloaded.", - ) - def get_metadata_file_datasets( - self, - trans=DependsOnTrans, - history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, - metadata_file: str = Query( - ..., - description="The name of the metadata file to retrieve.", - ), - ): - return self._get_metadata_file(trans, history_content_id, metadata_file) +@router.get( + "/api/datasets/{dataset_id}/converted/{ext}", + summary="Return information about datasets made by converting this dataset to a new format.", +) +def converted_ext( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + ext: str = Path( + ..., + description="File extension of the new format to convert this dataset to.", + ), + serialization_params: SerializationParams = Depends(query_serialization_params), + service: DatasetsService = depends(DatasetsService), +) -> AnyHDA: + """ + Return information about datasets made by converting this dataset to a new format. - def _get_metadata_file( - self, - trans, - history_content_id: DecodedDatabaseIdField, - metadata_file: str, - ) -> GalaxyFileResponse: - metadata_file_path, headers = self.service.get_metadata_file(trans, history_content_id, metadata_file) - return GalaxyFileResponse(path=cast(str, metadata_file_path), headers=headers) - - @router.get( - "/api/datasets/{dataset_id}", - summary="Displays information about and/or content of a dataset.", - ) - def show( - self, - request: Request, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - hda_ldda: DatasetSourceType = Query( - default=DatasetSourceType.hda, - description=("The type of information about the dataset to be requested."), - ), - data_type: Optional[RequestDataType] = Query( - default=None, - description=( - "The type of information about the dataset to be requested. " - "Each of these values may require additional parameters in the request and " - "may return different responses." - ), + If there is no existing converted dataset for the format in `ext`, one will be created. + + **Note**: `view` and `keys` are also available to control the serialization of the dataset. + """ + return service.converted_ext(trans, dataset_id, ext, serialization_params) + + +@router.get( + "/api/datasets/{dataset_id}/converted", + summary=("Return a a map with all the existing converted datasets associated with this instance."), +) +def converted( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + service: DatasetsService = depends(DatasetsService), +) -> ConvertedDatasetsMap: + """ + Return a map of ` : ` containing all the *existing* converted datasets. + """ + return service.converted(trans, dataset_id) + + +@router.put( + "/api/datasets/{dataset_id}/permissions", + summary="Set permissions of the given history dataset to the given role ids.", +) +def update_permissions( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + # Using a generic Dict here as an attempt on supporting multiple aliases for the permissions params. + payload: Dict[str, Any] = Body( + default=..., + example=UpdateDatasetPermissionsPayload(), + ), + service: DatasetsService = depends(DatasetsService), +) -> DatasetAssociationRoles: + """Set permissions of the given history dataset to the given role ids.""" + update_payload = get_update_permission_payload(payload) + return service.update_permissions(trans, dataset_id, update_payload) + + +@router.get( + "/api/histories/{history_id}/contents/{history_content_id}/extra_files", + summary="Get the list of extra files/directories associated with a dataset.", + tags=["histories"], +) +def extra_files_history( + trans=DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, + service: DatasetsService = depends(DatasetsService), +) -> DatasetExtraFiles: + return service.extra_files(trans, history_content_id) + + +@router.get( + "/api/datasets/{dataset_id}/extra_files", + summary="Get the list of extra files/directories associated with a dataset.", +) +def extra_files( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + service: DatasetsService = depends(DatasetsService), +) -> DatasetExtraFiles: + return service.extra_files(trans, dataset_id) + + +@router.get( + "/api/histories/{history_id}/contents/{history_content_id}/display", + name="history_contents_display", + summary="Displays (preview) or downloads dataset content.", + tags=["histories"], + response_class=StreamingResponse, +) +@router.head( + "/api/histories/{history_id}/contents/{history_content_id}/display", + name="history_contents_display", + summary="Check if dataset content can be previewed or downloaded.", + tags=["histories"], +) +def display_history_content( + request: Request, + trans=DependsOnTrans, + history_id: Optional[DecodedDatabaseIdField] = HistoryIDPathParam, + history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, + preview: bool = PreviewQueryParam, + filename: Optional[str] = FilenameQueryParam, + to_ext: Optional[str] = ToExtQueryParam, + raw: bool = RawQueryParam, + offset: Optional[int] = DisplayOffsetQueryParam, + ck_size: Optional[int] = DisplayChunkSizeQueryParam, + service: DatasetsService = depends(DatasetsService), +): + """Streams the dataset for download or the contents preview to be displayed in a browser.""" + return _display(request, trans, history_content_id, preview, filename, to_ext, raw, service, offset, ck_size) + + +@router.get( + "/api/datasets/{history_content_id}/display", + summary="Displays (preview) or downloads dataset content.", + response_class=StreamingResponse, +) +@router.head( + "/api/datasets/{history_content_id}/display", + summary="Check if dataset content can be previewed or downloaded.", +) +def display( + request: Request, + trans=DependsOnTrans, + history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, + preview: bool = PreviewQueryParam, + filename: Optional[str] = FilenameQueryParam, + to_ext: Optional[str] = ToExtQueryParam, + raw: bool = RawQueryParam, + offset: Optional[int] = DisplayOffsetQueryParam, + ck_size: Optional[int] = DisplayChunkSizeQueryParam, + service: DatasetsService = depends(DatasetsService), +): + """Streams the dataset for download or the contents preview to be displayed in a browser.""" + return _display(request, trans, history_content_id, preview, filename, to_ext, raw, service, offset, ck_size) + + +@router.get( + "/api/histories/{history_id}/contents/{history_content_id}/metadata_file", + summary="Returns the metadata file associated with this history item.", + name="get_metadata_file", + tags=["histories"], + operation_id="history_contents__get_metadata_file", + response_class=GalaxyFileResponse, +) +def get_metadata_file_history_content( + trans=DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, + metadata_file: str = Query( + ..., + description="The name of the metadata file to retrieve.", + ), + service: DatasetsService = depends(DatasetsService), +): + return _get_metadata_file(trans, history_content_id, metadata_file, service) + + +@router.get( + "/api/datasets/{history_content_id}/metadata_file", + summary="Returns the metadata file associated with this history item.", + response_class=GalaxyFileResponse, + operation_id="datasets__get_metadata_file", +) +@router.head( + "/api/datasets/{history_content_id}/metadata_file", + summary="Check if metadata file can be downloaded.", +) +def get_metadata_file_datasets( + trans=DependsOnTrans, + history_content_id: DecodedDatabaseIdField = DatasetIDPathParam, + metadata_file: str = Query( + ..., + description="The name of the metadata file to retrieve.", + ), + service: DatasetsService = depends(DatasetsService), +): + return _get_metadata_file(trans, history_content_id, metadata_file, service) + + +@router.get( + "/api/datasets/{dataset_id}", + summary="Displays information about and/or content of a dataset.", +) +def show( + request: Request, + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + hda_ldda: DatasetSourceType = Query( + default=DatasetSourceType.hda, + description=("The type of information about the dataset to be requested."), + ), + data_type: Optional[RequestDataType] = Query( + default=None, + description=( + "The type of information about the dataset to be requested. " + "Each of these values may require additional parameters in the request and " + "may return different responses." ), - serialization_params: SerializationParams = Depends(query_serialization_params), - ): - """ - **Note**: Due to the multipurpose nature of this endpoint, which can receive a wild variety of parameters - and return different kinds of responses, the documentation here will be limited. - To get more information please check the source code. - """ - exclude_params = {"hda_ldda", "data_type"} - exclude_params.update(SerializationParams.__fields__.keys()) - extra_params = get_query_parameters_from_request_excluding(request, exclude_params) - - return self.service.show(trans, dataset_id, hda_ldda, serialization_params, data_type, **extra_params) - - @router.get( - "/api/datasets/{dataset_id}/content/{content_type}", - summary="Retrieve information about the content of a dataset.", - ) - def get_structured_content( - self, - request: Request, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - content_type: DatasetContentType = DatasetContentType.data, - ): - content, headers = self.service.get_structured_content(trans, dataset_id, content_type, **request.query_params) - return Response(content=content, headers=headers) - - @router.delete( - "/api/datasets", - summary="Deletes or purges a batch of datasets.", + ), + serialization_params: SerializationParams = Depends(query_serialization_params), + service: DatasetsService = depends(DatasetsService), +): + """ + **Note**: Due to the multipurpose nature of this endpoint, which can receive a wild variety of parameters + and return different kinds of responses, the documentation here will be limited. + To get more information please check the source code. + """ + exclude_params = {"hda_ldda", "data_type"} + exclude_params.update(SerializationParams.__fields__.keys()) + extra_params = get_query_parameters_from_request_excluding(request, exclude_params) + + return service.show(trans, dataset_id, hda_ldda, serialization_params, data_type, **extra_params) + + +@router.get( + "/api/datasets/{dataset_id}/content/{content_type}", + summary="Retrieve information about the content of a dataset.", +) +def get_structured_content( + request: Request, + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + content_type: DatasetContentType = DatasetContentType.data, + service: DatasetsService = depends(DatasetsService), +): + content, headers = service.get_structured_content(trans, dataset_id, content_type, **request.query_params) + return Response(content=content, headers=headers) + + +@router.delete( + "/api/datasets", + summary="Deletes or purges a batch of datasets.", +) +def delete_batch( + trans=DependsOnTrans, + payload: DeleteDatasetBatchPayload = Body(...), + service: DatasetsService = depends(DatasetsService), +) -> DeleteDatasetBatchResult: + """ + Deletes or purges a batch of datasets. + **Warning**: only the ownership of the datasets (and upload state for HDAs) is checked, + no other checks or restrictions are made. + """ + return service.delete_batch(trans, payload) + + +@router.put( + "/api/datasets/{dataset_id}/hash", + summary="Compute dataset hash for dataset and update model", +) +def compute_hash( + trans=DependsOnTrans, + dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, + hda_ldda: DatasetSourceType = DatasetSourceQueryParam, + payload: ComputeDatasetHashPayload = Body(...), + service: DatasetsService = depends(DatasetsService), +) -> AsyncTaskResultSummary: + return service.compute_hash(trans, dataset_id, payload, hda_ldda=hda_ldda) + + +def _display( + request: Request, + trans, + history_content_id: DecodedDatabaseIdField, + preview: bool, + filename: Optional[str], + to_ext: Optional[str], + raw: bool, + service: DatasetsService, + offset: Optional[int] = None, + ck_size: Optional[int] = None, +): + extra_params = get_query_parameters_from_request_excluding( + request, {"preview", "filename", "to_ext", "raw", "dataset", "ck_size", "offset"} ) - def delete_batch( - self, - trans=DependsOnTrans, - payload: DeleteDatasetBatchPayload = Body(...), - ) -> DeleteDatasetBatchResult: - """ - Deletes or purges a batch of datasets. - **Warning**: only the ownership of the datasets (and upload state for HDAs) is checked, - no other checks or restrictions are made. - """ - return self.service.delete_batch(trans, payload) - - @router.put( - "/api/datasets/{dataset_id}/hash", - summary="Compute dataset hash for dataset and update model", + display_data, headers = service.display( + trans, + history_content_id, + preview=preview, + filename=filename, + to_ext=to_ext, + raw=raw, + offset=offset, + ck_size=ck_size, + **extra_params, ) - def compute_hash( - self, - trans=DependsOnTrans, - dataset_id: DecodedDatabaseIdField = DatasetIDPathParam, - hda_ldda: DatasetSourceType = DatasetSourceQueryParam, - payload: ComputeDatasetHashPayload = Body(...), - ) -> AsyncTaskResultSummary: - return self.service.compute_hash(trans, dataset_id, payload, hda_ldda=hda_ldda) + if isinstance(display_data, IOBase): + file_name = getattr(display_data, "name", None) + if file_name: + return GalaxyFileResponse(file_name, headers=headers, method=request.method) + elif isinstance(display_data, ZipstreamWrapper): + return StreamingResponse(display_data.response(), headers=headers) + elif isinstance(display_data, bytes): + return StreamingResponse(BytesIO(display_data), headers=headers) + elif isinstance(display_data, str): + return StreamingResponse(content=StringIO(display_data), headers=headers) + return StreamingResponse(display_data, headers=headers) + + +def _get_metadata_file( + trans, + history_content_id: DecodedDatabaseIdField, + metadata_file: str, + service: DatasetsService, +) -> GalaxyFileResponse: + metadata_file_path, headers = service.get_metadata_file(trans, history_content_id, metadata_file) + return GalaxyFileResponse(path=cast(str, metadata_file_path), headers=headers) diff --git a/lib/galaxy/webapps/galaxy/api/datatypes.py b/lib/galaxy/webapps/galaxy/api/datatypes.py index 7a35bc4b0a6a..000e50bfd5ee 100644 --- a/lib/galaxy/webapps/galaxy/api/datatypes.py +++ b/lib/galaxy/webapps/galaxy/api/datatypes.py @@ -54,104 +54,108 @@ ) -@router.cbv -class FastAPIDatatypes: - datatypes_registry: Registry = depends(Registry) - - @router.get( - "/api/datatypes", - summary="Lists all available data types", - response_description="List of data types", - ) - async def index( - self, - extension_only: Optional[bool] = ExtensionOnlyQueryParam, - upload_only: Optional[bool] = UploadOnlyQueryParam, - ) -> Union[List[DatatypeDetails], List[str]]: - """Gets the list of all available data types.""" - return view_index(self.datatypes_registry, extension_only, upload_only) - - @router.get( - "/api/datatypes/mapping", - summary="Returns mappings for data types and their implementing classes", - response_description="Dictionary to map data types with their classes", - ) - async def mapping(self) -> DatatypesMap: - """Gets mappings for data types.""" - return view_mapping(self.datatypes_registry) - - @router.get( - "/api/datatypes/types_and_mapping", - summary="Returns all the data types extensions and their mappings", - response_description="Dictionary to map data types with their classes", - ) - async def types_and_mapping( - self, - extension_only: Optional[bool] = ExtensionOnlyQueryParam, - upload_only: Optional[bool] = UploadOnlyQueryParam, - ) -> DatatypesCombinedMap: - """Combines the datatype information from (/api/datatypes) and the - mapping information from (/api/datatypes/mapping) into a single - response.""" - return DatatypesCombinedMap( - datatypes=view_index(self.datatypes_registry, extension_only, upload_only), - datatypes_mapping=view_mapping(self.datatypes_registry), - ) - - @router.get( - "/api/datatypes/sniffers", - summary="Returns the list of all installed sniffers", - response_description="List of datatype sniffers", - ) - async def sniffers(self) -> List[str]: - """Gets the list of all installed data type sniffers.""" - return view_sniffers(self.datatypes_registry) - - @router.get( - "/api/datatypes/converters", - summary="Returns the list of all installed converters", - response_description="List of all datatype converters", - ) - async def converters(self) -> DatatypeConverterList: - """Gets the list of all installed converters.""" - return view_converters(self.datatypes_registry) - - @router.get( - "/api/datatypes/edam_formats", - summary="Returns a dictionary/map of datatypes and EDAM formats", - response_description="Dictionary/map of datatypes and EDAM formats", - ) - async def edam_formats(self) -> Dict[str, str]: - """Gets a map of datatypes and their corresponding EDAM formats.""" - return cast(Dict[str, str], view_edam_formats(self.datatypes_registry)) - - @router.get( - "/api/datatypes/edam_formats/detailed", - summary="Returns a dictionary of datatypes and EDAM format details", - response_description="Dictionary of EDAM format details containing the EDAM iri, label, and definition", - response_model=DatatypesEDAMDetailsDict, - ) - async def edam_formats_detailed(self): - """Gets a map of datatypes and their corresponding EDAM formats. - EDAM formats contain the EDAM iri, label, and definition.""" - return view_edam_formats(self.datatypes_registry, True) - - @router.get( - "/api/datatypes/edam_data", - summary="Returns a dictionary/map of datatypes and EDAM data", - response_description="Dictionary/map of datatypes and EDAM data", - ) - async def edam_data(self) -> Dict[str, str]: - """Gets a map of datatypes and their corresponding EDAM data.""" - return cast(Dict[str, str], view_edam_data(self.datatypes_registry)) - - @router.get( - "/api/datatypes/edam_data/detailed", - summary="Returns a dictionary of datatypes and EDAM data details", - response_description="Dictionary of EDAM data details containing the EDAM iri, label, and definition", - response_model=DatatypesEDAMDetailsDict, +@router.get( + "/api/datatypes", + summary="Lists all available data types", + response_description="List of data types", +) +async def index( + extension_only: Optional[bool] = ExtensionOnlyQueryParam, + upload_only: Optional[bool] = UploadOnlyQueryParam, + datatypes_registry: Registry = depends(Registry), +) -> Union[List[DatatypeDetails], List[str]]: + """Gets the list of all available data types.""" + return view_index(datatypes_registry, extension_only, upload_only) + + +@router.get( + "/api/datatypes/mapping", + summary="Returns mappings for data types and their implementing classes", + response_description="Dictionary to map data types with their classes", +) +async def mapping(datatypes_registry: Registry = depends(Registry)) -> DatatypesMap: + """Gets mappings for data types.""" + return view_mapping(datatypes_registry) + + +@router.get( + "/api/datatypes/types_and_mapping", + summary="Returns all the data types extensions and their mappings", + response_description="Dictionary to map data types with their classes", +) +async def types_and_mapping( + extension_only: Optional[bool] = ExtensionOnlyQueryParam, + upload_only: Optional[bool] = UploadOnlyQueryParam, + datatypes_registry: Registry = depends(Registry), +) -> DatatypesCombinedMap: + """Combines the datatype information from (/api/datatypes) and the + mapping information from (/api/datatypes/mapping) into a single + response.""" + return DatatypesCombinedMap( + datatypes=view_index(datatypes_registry, extension_only, upload_only), + datatypes_mapping=view_mapping(datatypes_registry), ) - async def edam_data_detailed(self): - """Gets a map of datatypes and their corresponding EDAM data. - EDAM data contains the EDAM iri, label, and definition.""" - return view_edam_data(self.datatypes_registry, True) + + +@router.get( + "/api/datatypes/sniffers", + summary="Returns the list of all installed sniffers", + response_description="List of datatype sniffers", +) +async def sniffers(datatypes_registry: Registry = depends(Registry)) -> List[str]: + """Gets the list of all installed data type sniffers.""" + return view_sniffers(datatypes_registry) + + +@router.get( + "/api/datatypes/converters", + summary="Returns the list of all installed converters", + response_description="List of all datatype converters", +) +async def converters(datatypes_registry: Registry = depends(Registry)) -> DatatypeConverterList: + """Gets the list of all installed converters.""" + return view_converters(datatypes_registry) + + +@router.get( + "/api/datatypes/edam_formats", + summary="Returns a dictionary/map of datatypes and EDAM formats", + response_description="Dictionary/map of datatypes and EDAM formats", +) +async def edam_formats(datatypes_registry: Registry = depends(Registry)) -> Dict[str, str]: + """Gets a map of datatypes and their corresponding EDAM formats.""" + return cast(Dict[str, str], view_edam_formats(datatypes_registry)) + + +@router.get( + "/api/datatypes/edam_formats/detailed", + summary="Returns a dictionary of datatypes and EDAM format details", + response_description="Dictionary of EDAM format details containing the EDAM iri, label, and definition", + response_model=DatatypesEDAMDetailsDict, +) +async def edam_formats_detailed(datatypes_registry: Registry = depends(Registry)): + """Gets a map of datatypes and their corresponding EDAM formats. + EDAM formats contain the EDAM iri, label, and definition.""" + return view_edam_formats(datatypes_registry, True) + + +@router.get( + "/api/datatypes/edam_data", + summary="Returns a dictionary/map of datatypes and EDAM data", + response_description="Dictionary/map of datatypes and EDAM data", +) +async def edam_data(datatypes_registry: Registry = depends(Registry)) -> Dict[str, str]: + """Gets a map of datatypes and their corresponding EDAM data.""" + return cast(Dict[str, str], view_edam_data(datatypes_registry)) + + +@router.get( + "/api/datatypes/edam_data/detailed", + summary="Returns a dictionary of datatypes and EDAM data details", + response_description="Dictionary of EDAM data details containing the EDAM iri, label, and definition", + response_model=DatatypesEDAMDetailsDict, +) +async def edam_data_detailed(datatypes_registry: Registry = depends(Registry)): + """Gets a map of datatypes and their corresponding EDAM data. + EDAM data contains the EDAM iri, label, and definition.""" + return view_edam_data(datatypes_registry, True) diff --git a/lib/galaxy/webapps/galaxy/api/display_applications.py b/lib/galaxy/webapps/galaxy/api/display_applications.py index b08a58d0a8b8..3ee0239a2753 100644 --- a/lib/galaxy/webapps/galaxy/api/display_applications.py +++ b/lib/galaxy/webapps/galaxy/api/display_applications.py @@ -25,37 +25,34 @@ router = Router(tags=["display_applications"]) -@router.cbv -class FastAPIDisplay: - manager: DisplayApplicationsManager = depends(DisplayApplicationsManager) - - @router.get( - "/api/display_applications", - summary="Returns the list of display applications.", - name="display_applications_index", - ) - def index( - self, - ) -> List[DisplayApplication]: - """ - Returns the list of display applications. - """ - return self.manager.index() - - @router.post( - "/api/display_applications/reload", - summary="Reloads the list of display applications.", - name="display_applications_reload", - require_admin=True, - ) - def reload( - self, - payload: Optional[Dict[str, List[str]]] = Body(default=None), - ) -> ReloadFeedback: - """ - Reloads the list of display applications. - """ - payload = payload or {} - ids = payload.get("ids", []) - result = self.manager.reload(ids) - return result +@router.get( + "/api/display_applications", + summary="Returns the list of display applications.", + name="display_applications_index", +) +def index( + manager: DisplayApplicationsManager = depends(DisplayApplicationsManager), +) -> List[DisplayApplication]: + """ + Returns the list of display applications. + """ + return manager.index() + + +@router.post( + "/api/display_applications/reload", + summary="Reloads the list of display applications.", + name="display_applications_reload", + require_admin=True, +) +def reload( + payload: Optional[Dict[str, List[str]]] = Body(default=None), + manager: DisplayApplicationsManager = depends(DisplayApplicationsManager), +) -> ReloadFeedback: + """ + Reloads the list of display applications. + """ + payload = payload or {} + ids = payload.get("ids", []) + result = manager.reload(ids) + return result diff --git a/lib/galaxy/webapps/galaxy/api/drs.py b/lib/galaxy/webapps/galaxy/api/drs.py index d0efa39c62e8..e4174575e470 100644 --- a/lib/galaxy/webapps/galaxy/api/drs.py +++ b/lib/galaxy/webapps/galaxy/api/drs.py @@ -38,73 +38,73 @@ DRS_SERVICE_DESCRIPTION = "Serves Galaxy datasets according to the GA4GH DRS specification" -@router.cbv -class DrsApi: - service: DatasetsService = depends(DatasetsService) - config: GalaxyAppConfiguration = depends(GalaxyAppConfiguration) +@router.get("/ga4gh/drs/v1/service-info") +def service_info(request: Request, config: GalaxyAppConfiguration = depends(GalaxyAppConfiguration)) -> Service: + components = request.url.components + hostname = components.hostname + assert hostname + default_organization_id = ".".join(reversed(hostname.split("."))) + organization_id = config.ga4gh_service_id or default_organization_id + organization_name = config.organization_name or organization_id + organization_url = config.organization_url or f"{components.scheme}://{components.netloc}" - @router.get("/ga4gh/drs/v1/service-info") - def service_info(self, request: Request) -> Service: - components = request.url.components - hostname = components.hostname - assert hostname - default_organization_id = ".".join(reversed(hostname.split("."))) - config = self.config - organization_id = config.ga4gh_service_id or default_organization_id - organization_name = config.organization_name or organization_id - organization_url = config.organization_url or f"{components.scheme}://{components.netloc}" + organization = ServiceOrganization( + url=organization_url, + name=organization_name, + ) + service_type = ServiceType( + group="org.ga4gh", + artifact="drs", + version="1.2.0", + ) + extra_kwds = {} + if environment := config.ga4gh_service_environment: + extra_kwds["environment"] = environment + return Service( + id=organization_id + ".drs", + name=DRS_SERVICE_NAME, + description=DRS_SERVICE_DESCRIPTION, + organization=organization, + type=service_type, + version=VERSION, + **extra_kwds, + ) - organization = ServiceOrganization( - url=organization_url, - name=organization_name, - ) - service_type = ServiceType( - group="org.ga4gh", - artifact="drs", - version="1.2.0", - ) - extra_kwds = {} - if environment := config.ga4gh_service_environment: - extra_kwds["environment"] = environment - return Service( - id=organization_id + ".drs", - name=DRS_SERVICE_NAME, - description=DRS_SERVICE_DESCRIPTION, - organization=organization, - type=service_type, - version=VERSION, - **extra_kwds, - ) - @router.get("/ga4gh/drs/v1/objects/{object_id}") - @router.post("/ga4gh/drs/v1/objects/{object_id}") # spec specifies both get and post should work. - def get_object( - self, - request: Request, - trans: ProvidesHistoryContext = DependsOnTrans, - object_id: str = ObjectIDParam, - ) -> DrsObject: - return self.service.get_drs_object(trans, object_id, request_url=request.url) +@router.get("/ga4gh/drs/v1/objects/{object_id}") +@router.post("/ga4gh/drs/v1/objects/{object_id}") # spec specifies both get and post should work. +def get_object( + request: Request, + trans: ProvidesHistoryContext = DependsOnTrans, + object_id: str = ObjectIDParam, + service: DatasetsService = depends(DatasetsService), +) -> DrsObject: + return service.get_drs_object(trans, object_id, request_url=request.url) - @router.get("/ga4gh/drs/v1/objects/{object_id}/access/{access_id}") - @router.post("/ga4gh/drs/v1/objects/{object_id}/access/{access_id}") - def get_access_url( - self, - request: Request, - trans: ProvidesHistoryContext = DependsOnTrans, - object_id: str = ObjectIDParam, - access_id: str = AccessIDParam, - ): - raise ObjectNotFound("Access IDs are not implemented for this DRS server") - @router.get( - "/api/drs_download/{object_id}", - response_class=FileResponse, +@router.get("/ga4gh/drs/v1/objects/{object_id}/access/{access_id}") +@router.post("/ga4gh/drs/v1/objects/{object_id}/access/{access_id}") +def get_access_url( + request: Request, + trans: ProvidesHistoryContext = DependsOnTrans, + object_id: str = ObjectIDParam, + access_id: str = AccessIDParam, +): + raise ObjectNotFound("Access IDs are not implemented for this DRS server") + + +@router.get( + "/api/drs_download/{object_id}", + response_class=FileResponse, +) +def download( + trans: ProvidesHistoryContext = DependsOnTrans, + object_id: str = ObjectIDParam, + service: DatasetsService = depends(DatasetsService), +): + decoded_object_id, hda_ldda = service.drs_dataset_instance(object_id) + display_data, headers = service.display( + trans, DecodedDatabaseIdField(decoded_object_id), hda_ldda=hda_ldda, filename=None, raw=True ) - def download(self, trans: ProvidesHistoryContext = DependsOnTrans, object_id: str = ObjectIDParam): - decoded_object_id, hda_ldda = self.service.drs_dataset_instance(object_id) - display_data, headers = self.service.display( - trans, DecodedDatabaseIdField(decoded_object_id), hda_ldda=hda_ldda, filename=None, raw=True - ) - data_io = cast(IOBase, display_data) - return FileResponse(getattr(data_io, "name", "unnamed_file"), headers=headers) + data_io = cast(IOBase, display_data) + return FileResponse(getattr(data_io, "name", "unnamed_file"), headers=headers) diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index f67c12749a8d..7f0419e8575f 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -66,62 +66,59 @@ ) -@router.cbv -class FastAPILibraryFoldersContents: - service: LibraryFolderContentsService = depends(LibraryFolderContentsService) - - @router.get( - "/api/folders/{folder_id}/contents", - summary="Returns a list of a folder's contents (files and sub-folders) with additional metadata about the folder.", - responses={ - 200: { - "description": "The contents of the folder that match the query parameters.", - "model": LibraryFolderContentsIndexResult, - }, +@router.get( + "/api/folders/{folder_id}/contents", + summary="Returns a list of a folder's contents (files and sub-folders) with additional metadata about the folder.", + responses={ + 200: { + "description": "The contents of the folder that match the query parameters.", + "model": LibraryFolderContentsIndexResult, }, + }, +) +def index( + trans: ProvidesUserContext = DependsOnTrans, + folder_id: LibraryFolderDatabaseIdField = FolderIdPathParam, + limit: int = LimitQueryParam, + offset: int = OffsetQueryParam, + search_text: Optional[str] = SearchQueryParam, + include_deleted: Optional[bool] = IncludeDeletedQueryParam, + order_by: LibraryFolderContentsIndexSortByEnum = SortByQueryParam, + sort_desc: Optional[bool] = SortDescQueryParam, + service: LibraryFolderContentsService = depends(LibraryFolderContentsService), +): + """Returns a list of a folder's contents (files and sub-folders). + + Additional metadata for the folder is provided in the response as a separate object containing data + for breadcrumb path building, permissions and other folder's details. + + *Note*: When sorting, folders always have priority (they show-up before any dataset regardless of the sorting). + + **Security note**: + - Accessing a library folder or sub-folder requires only access to the parent library. + - Deleted folders can only be accessed by admins or users with `MODIFY` permission. + - Datasets may be public, private or restricted (to a group of users). Listing deleted datasets has the same requirements as folders. + """ + payload = LibraryFolderContentsIndexQueryPayload( + limit=limit, + offset=offset, + search_text=search_text, + include_deleted=include_deleted, + order_by=order_by, + sort_desc=sort_desc, ) - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - folder_id: LibraryFolderDatabaseIdField = FolderIdPathParam, - limit: int = LimitQueryParam, - offset: int = OffsetQueryParam, - search_text: Optional[str] = SearchQueryParam, - include_deleted: Optional[bool] = IncludeDeletedQueryParam, - order_by: LibraryFolderContentsIndexSortByEnum = SortByQueryParam, - sort_desc: Optional[bool] = SortDescQueryParam, - ): - """Returns a list of a folder's contents (files and sub-folders). - - Additional metadata for the folder is provided in the response as a separate object containing data - for breadcrumb path building, permissions and other folder's details. - - *Note*: When sorting, folders always have priority (they show-up before any dataset regardless of the sorting). - - **Security note**: - - Accessing a library folder or sub-folder requires only access to the parent library. - - Deleted folders can only be accessed by admins or users with `MODIFY` permission. - - Datasets may be public, private or restricted (to a group of users). Listing deleted datasets has the same requirements as folders. - """ - payload = LibraryFolderContentsIndexQueryPayload( - limit=limit, - offset=offset, - search_text=search_text, - include_deleted=include_deleted, - order_by=order_by, - sort_desc=sort_desc, - ) - return self.service.index(trans, folder_id, payload) - - @router.post( - "/api/folders/{folder_id}/contents", - name="add_history_datasets_to_library", - summary="Creates a new library file from an existing HDA/HDCA.", - ) - def create( - self, - trans: ProvidesUserContext = DependsOnTrans, - folder_id: LibraryFolderDatabaseIdField = FolderIdPathParam, - payload: CreateLibraryFilePayload = Body(...), - ): - return self.service.create(trans, folder_id, payload) + return service.index(trans, folder_id, payload) + + +@router.post( + "/api/folders/{folder_id}/contents", + name="add_history_datasets_to_library", + summary="Creates a new library file from an existing HDA/HDCA.", +) +def create( + trans: ProvidesUserContext = DependsOnTrans, + folder_id: LibraryFolderDatabaseIdField = FolderIdPathParam, + payload: CreateLibraryFilePayload = Body(...), + service: LibraryFolderContentsService = depends(LibraryFolderContentsService), +): + return service.create(trans, folder_id, payload) diff --git a/lib/galaxy/webapps/galaxy/api/folders.py b/lib/galaxy/webapps/galaxy/api/folders.py index 0f8f204c829e..a2dc11ea3e10 100644 --- a/lib/galaxy/webapps/galaxy/api/folders.py +++ b/lib/galaxy/webapps/galaxy/api/folders.py @@ -45,116 +45,117 @@ ) -@router.cbv -class FastAPILibraryFolders: - service: LibraryFoldersService = depends(LibraryFoldersService) +@router.get( + "/api/folders/{id}", + summary="Displays information about a particular library folder.", +) +def show( + trans: ProvidesUserContext = DependsOnTrans, + id: LibraryFolderDatabaseIdField = FolderIdPathParam, + service: LibraryFoldersService = depends(LibraryFoldersService), +) -> LibraryFolderDetails: + """Returns detailed information about the library folder with the given ID.""" + return service.show(trans, id) - @router.get( - "/api/folders/{id}", - summary="Displays information about a particular library folder.", - ) - def show( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: LibraryFolderDatabaseIdField = FolderIdPathParam, - ) -> LibraryFolderDetails: - """Returns detailed information about the library folder with the given ID.""" - return self.service.show(trans, id) - - @router.post( - "/api/folders/{id}", - summary="Create a new library folder underneath the one specified by the ID.", - ) - def create( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: LibraryFolderDatabaseIdField = FolderIdPathParam, - payload: CreateLibraryFolderPayload = Body(...), - ) -> LibraryFolderDetails: - """Returns detailed information about the newly created library folder.""" - return self.service.create(trans, id, payload) - - @router.put( - "/api/folders/{id}", - summary="Updates the information of an existing library folder.", - ) - @router.patch("/api/folders/{id}") - def update( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: LibraryFolderDatabaseIdField = FolderIdPathParam, - payload: UpdateLibraryFolderPayload = Body(...), - ) -> LibraryFolderDetails: - """Updates the information of an existing library folder.""" - return self.service.update(trans, id, payload) - - @router.delete( - "/api/folders/{id}", - summary="Marks the specified library folder as deleted (or undeleted).", - ) - def delete( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: LibraryFolderDatabaseIdField = FolderIdPathParam, - undelete: Optional[bool] = UndeleteQueryParam, - ) -> LibraryFolderDetails: - """Marks the specified library folder as deleted (or undeleted).""" - return self.service.delete(trans, id, undelete) - - @router.get( - "/api/folders/{id}/permissions", - summary="Gets the current or available permissions of a particular library folder.", - ) - def get_permissions( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: LibraryFolderDatabaseIdField = FolderIdPathParam, - scope: Optional[LibraryPermissionScope] = Query( - None, - title="Scope", - description="The scope of the permissions to retrieve. Either the `current` permissions or the `available`.", - ), - page: int = Query( - default=1, title="Page", description="The page number to retrieve when paginating the available roles." - ), - page_limit: int = Query( - default=10, title="Page Limit", description="The maximum number of permissions per page when paginating." - ), - q: Optional[str] = Query( - None, title="Query", description="Optional search text to retrieve only the roles matching this query." - ), - ) -> Union[LibraryFolderCurrentPermissions, LibraryAvailablePermissions]: - """Gets the current or available permissions of a particular library. - The results can be paginated and additionally filtered by a query.""" - return self.service.get_permissions( - trans, - id, - scope, - page, - page_limit, - q, - ) - - @router.post( - "/api/folders/{id}/permissions", - summary="Sets the permissions to manage a library folder.", + +@router.post( + "/api/folders/{id}", + summary="Create a new library folder underneath the one specified by the ID.", +) +def create( + trans: ProvidesUserContext = DependsOnTrans, + id: LibraryFolderDatabaseIdField = FolderIdPathParam, + payload: CreateLibraryFolderPayload = Body(...), + service: LibraryFoldersService = depends(LibraryFoldersService), +) -> LibraryFolderDetails: + """Returns detailed information about the newly created library folder.""" + return service.create(trans, id, payload) + + +@router.put( + "/api/folders/{id}", + summary="Updates the information of an existing library folder.", +) +@router.patch("/api/folders/{id}") +def update( + trans: ProvidesUserContext = DependsOnTrans, + id: LibraryFolderDatabaseIdField = FolderIdPathParam, + payload: UpdateLibraryFolderPayload = Body(...), + service: LibraryFoldersService = depends(LibraryFoldersService), +) -> LibraryFolderDetails: + """Updates the information of an existing library folder.""" + return service.update(trans, id, payload) + + +@router.delete( + "/api/folders/{id}", + summary="Marks the specified library folder as deleted (or undeleted).", +) +def delete( + trans: ProvidesUserContext = DependsOnTrans, + id: LibraryFolderDatabaseIdField = FolderIdPathParam, + undelete: Optional[bool] = UndeleteQueryParam, + service: LibraryFoldersService = depends(LibraryFoldersService), +) -> LibraryFolderDetails: + """Marks the specified library folder as deleted (or undeleted).""" + return service.delete(trans, id, undelete) + + +@router.get( + "/api/folders/{id}/permissions", + summary="Gets the current or available permissions of a particular library folder.", +) +def get_permissions( + trans: ProvidesUserContext = DependsOnTrans, + id: LibraryFolderDatabaseIdField = FolderIdPathParam, + scope: Optional[LibraryPermissionScope] = Query( + None, + title="Scope", + description="The scope of the permissions to retrieve. Either the `current` permissions or the `available`.", + ), + page: int = Query( + default=1, title="Page", description="The page number to retrieve when paginating the available roles." + ), + page_limit: int = Query( + default=10, title="Page Limit", description="The maximum number of permissions per page when paginating." + ), + q: Optional[str] = Query( + None, title="Query", description="Optional search text to retrieve only the roles matching this query." + ), + service: LibraryFoldersService = depends(LibraryFoldersService), +) -> Union[LibraryFolderCurrentPermissions, LibraryAvailablePermissions]: + """Gets the current or available permissions of a particular library. + The results can be paginated and additionally filtered by a query.""" + return service.get_permissions( + trans, + id, + scope, + page, + page_limit, + q, ) - def set_permissions( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: LibraryFolderDatabaseIdField = FolderIdPathParam, - action: Optional[LibraryFolderPermissionAction] = Query( - default=None, - title="Action", - description=( - "Indicates what action should be performed on the Library. " - f"Currently only `{LibraryFolderPermissionAction.set_permissions.value}` is supported." - ), + + +@router.post( + "/api/folders/{id}/permissions", + summary="Sets the permissions to manage a library folder.", +) +def set_permissions( + trans: ProvidesUserContext = DependsOnTrans, + id: LibraryFolderDatabaseIdField = FolderIdPathParam, + action: Optional[LibraryFolderPermissionAction] = Query( + default=None, + title="Action", + description=( + "Indicates what action should be performed on the Library. " + f"Currently only `{LibraryFolderPermissionAction.set_permissions.value}` is supported." ), - payload: LibraryFolderPermissionsPayload = Body(...), - ) -> LibraryFolderCurrentPermissions: - """Sets the permissions to manage a library folder.""" - payload_dict = payload.dict(by_alias=True) - if isinstance(payload, LibraryFolderPermissionsPayload) and action is not None: - payload_dict["action"] = action - return self.service.set_permissions(trans, id, payload_dict) + ), + payload: LibraryFolderPermissionsPayload = Body(...), + service: LibraryFoldersService = depends(LibraryFoldersService), +) -> LibraryFolderCurrentPermissions: + """Sets the permissions to manage a library folder.""" + payload_dict = payload.dict(by_alias=True) + if isinstance(payload, LibraryFolderPermissionsPayload) and action is not None: + payload_dict["action"] = action + return service.set_permissions(trans, id, payload_dict) diff --git a/lib/galaxy/webapps/galaxy/api/forms.py b/lib/galaxy/webapps/galaxy/api/forms.py index 2e89295ebc01..b2c7299458a9 100644 --- a/lib/galaxy/webapps/galaxy/api/forms.py +++ b/lib/galaxy/webapps/galaxy/api/forms.py @@ -26,19 +26,24 @@ router = Router(tags=["forms"]) -@router.cbv -class FastAPIForms: - form_manager: FormManager = depends(FormManager) +@router.delete("/api/forms/{id}", require_admin=True) +def delete( + id: DecodedDatabaseIdField, + trans: ProvidesUserContext = DependsOnTrans, + form_manager: FormManager = depends(FormManager), +): + form = form_manager.get(trans, id) + form_manager.delete(trans, form) - @router.delete("/api/forms/{id}", require_admin=True) - def delete(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans): - form = self.form_manager.get(trans, id) - self.form_manager.delete(trans, form) - @router.post("/api/forms/{id}/undelete", require_admin=True) - def undelete(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans): - form = self.form_manager.get(trans, id) - self.form_manager.undelete(trans, form) +@router.post("/api/forms/{id}/undelete", require_admin=True) +def undelete( + id: DecodedDatabaseIdField, + trans: ProvidesUserContext = DependsOnTrans, + form_manager: FormManager = depends(FormManager), +): + form = form_manager.get(trans, id) + form_manager.undelete(trans, form) class FormDefinitionAPIController(BaseGalaxyAPIController): diff --git a/lib/galaxy/webapps/galaxy/api/genomes.py b/lib/galaxy/webapps/galaxy/api/genomes.py index b689d50392da..6d08d5162b34 100644 --- a/lib/galaxy/webapps/galaxy/api/genomes.py +++ b/lib/galaxy/webapps/galaxy/api/genomes.py @@ -64,63 +64,62 @@ def get_id(base, format): return base -@router.cbv -class FastAPIGenomes: - manager: GenomesManager = depends(GenomesManager) - - @router.get("/api/genomes", summary="Return a list of installed genomes", response_description="Installed genomes") - def index( - self, trans: ProvidesUserContext = DependsOnTrans, chrom_info: bool = ChromInfoQueryParam - ) -> List[List[str]]: - return self.manager.get_dbkeys(trans.user, chrom_info) - - @router.get( - "/api/genomes/{id}", - summary="Return information about build ", - response_description="Information about genome build ", - ) - def show( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: str = IdPathParam, - reference: bool = ReferenceQueryParam, - num: int = NumQueryParam, - chrom: str = ChromQueryParam, - low: int = LowQueryParam, - high: int = HighQueryParam, - format: str = FormatQueryParam, - ) -> Any: - id = get_id(id, format) - return self.manager.get_genome(trans, id, num, chrom, low, high, reference) - - @router.get( - "/api/genomes/{id}/indexes", - summary="Return all available indexes for a genome id for provided type", - response_description="Indexes for a genome id for provided type", - ) - def indexes( - self, - id: str = IdPathParam, - type: str = IndexTypeQueryParam, - format: str = FormatQueryParam, - ) -> Any: - id = get_id(id, format) - rval = self.manager.get_indexes(id, type) - return Response(rval) - - @router.get( - "/api/genomes/{id}/sequences", summary="Return raw sequence data", response_description="Raw sequence data" - ) - def sequences( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: str = IdPathParam, - reference: bool = ReferenceQueryParam, - chrom: str = ChromQueryParam, - low: int = LowQueryParam, - high: int = HighQueryParam, - format: str = FormatQueryParam, - ) -> Any: - id = get_id(id, format) - rval = self.manager.get_sequence(trans, id, chrom, low, high) - return Response(rval) +@router.get("/api/genomes", summary="Return a list of installed genomes", response_description="Installed genomes") +def index( + trans: ProvidesUserContext = DependsOnTrans, + chrom_info: bool = ChromInfoQueryParam, + manager: GenomesManager = depends(GenomesManager), +) -> List[List[str]]: + return manager.get_dbkeys(trans.user, chrom_info) + + +@router.get( + "/api/genomes/{id}", + summary="Return information about build ", + response_description="Information about genome build ", +) +def show( + trans: ProvidesUserContext = DependsOnTrans, + id: str = IdPathParam, + reference: bool = ReferenceQueryParam, + num: int = NumQueryParam, + chrom: str = ChromQueryParam, + low: int = LowQueryParam, + high: int = HighQueryParam, + format: str = FormatQueryParam, + manager: GenomesManager = depends(GenomesManager), +) -> Any: + id = get_id(id, format) + return manager.get_genome(trans, id, num, chrom, low, high, reference) + + +@router.get( + "/api/genomes/{id}/indexes", + summary="Return all available indexes for a genome id for provided type", + response_description="Indexes for a genome id for provided type", +) +def indexes( + id: str = IdPathParam, + type: str = IndexTypeQueryParam, + format: str = FormatQueryParam, + manager: GenomesManager = depends(GenomesManager), +) -> Any: + id = get_id(id, format) + rval = manager.get_indexes(id, type) + return Response(rval) + + +@router.get("/api/genomes/{id}/sequences", summary="Return raw sequence data", response_description="Raw sequence data") +def sequences( + trans: ProvidesUserContext = DependsOnTrans, + id: str = IdPathParam, + reference: bool = ReferenceQueryParam, + chrom: str = ChromQueryParam, + low: int = LowQueryParam, + high: int = HighQueryParam, + format: str = FormatQueryParam, + manager: GenomesManager = depends(GenomesManager), +) -> Any: + id = get_id(id, format) + rval = manager.get_sequence(trans, id, chrom, low, high) + return Response(rval) diff --git a/lib/galaxy/webapps/galaxy/api/group_roles.py b/lib/galaxy/webapps/galaxy/api/group_roles.py index 7ebe74725798..9b258e54727a 100644 --- a/lib/galaxy/webapps/galaxy/api/group_roles.py +++ b/lib/galaxy/webapps/galaxy/api/group_roles.py @@ -35,53 +35,54 @@ def group_role_to_model(trans, group_id: int, role) -> GroupRoleResponse: return GroupRoleResponse(id=role.id, name=role.name, url=url) -@router.cbv -class FastAPIGroupRoles: - manager: GroupRolesManager = depends(GroupRolesManager) - - @router.get( - "/api/groups/{group_id}/roles", - require_admin=True, - summary="Displays a collection (list) of groups.", - name="group_roles", - ) - def index( - self, trans: ProvidesAppContext = DependsOnTrans, group_id: DecodedDatabaseIdField = GroupIDParam - ) -> GroupRoleListResponse: - group_roles = self.manager.index(trans, group_id) - return GroupRoleListResponse(__root__=[group_role_to_model(trans, group_id, gr.role) for gr in group_roles]) - - @router.get( - "/api/groups/{group_id}/roles/{role_id}", - name="group_role", - require_admin=True, - summary="Displays information about a group role.", - ) - def show( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = GroupIDParam, - role_id: DecodedDatabaseIdField = RoleIDParam, - ) -> GroupRoleResponse: - role = self.manager.show(trans, role_id, group_id) - return group_role_to_model(trans, group_id, role) - - @router.put("/api/groups/{group_id}/roles/{role_id}", require_admin=True, summary="Adds a role to a group") - def update( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = GroupIDParam, - role_id: DecodedDatabaseIdField = RoleIDParam, - ) -> GroupRoleResponse: - role = self.manager.update(trans, role_id, group_id) - return group_role_to_model(trans, group_id, role) - - @router.delete("/api/groups/{group_id}/roles/{role_id}", require_admin=True, summary="Removes a role from a group") - def delete( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = GroupIDParam, - role_id: DecodedDatabaseIdField = RoleIDParam, - ) -> GroupRoleResponse: - role = self.manager.delete(trans, role_id, group_id) - return group_role_to_model(trans, group_id, role) +@router.get( + "/api/groups/{group_id}/roles", + require_admin=True, + summary="Displays a collection (list) of groups.", + name="group_roles", +) +def index( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + manager: GroupRolesManager = depends(GroupRolesManager), +) -> GroupRoleListResponse: + group_roles = manager.index(trans, group_id) + return GroupRoleListResponse(__root__=[group_role_to_model(trans, group_id, gr.role) for gr in group_roles]) + + +@router.get( + "/api/groups/{group_id}/roles/{role_id}", + name="group_role", + require_admin=True, + summary="Displays information about a group role.", +) +def show( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + role_id: DecodedDatabaseIdField = RoleIDParam, + manager: GroupRolesManager = depends(GroupRolesManager), +) -> GroupRoleResponse: + role = manager.show(trans, role_id, group_id) + return group_role_to_model(trans, group_id, role) + + +@router.put("/api/groups/{group_id}/roles/{role_id}", require_admin=True, summary="Adds a role to a group") +def update( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + role_id: DecodedDatabaseIdField = RoleIDParam, + manager: GroupRolesManager = depends(GroupRolesManager), +) -> GroupRoleResponse: + role = manager.update(trans, role_id, group_id) + return group_role_to_model(trans, group_id, role) + + +@router.delete("/api/groups/{group_id}/roles/{role_id}", require_admin=True, summary="Removes a role from a group") +def delete( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + role_id: DecodedDatabaseIdField = RoleIDParam, + manager: GroupRolesManager = depends(GroupRolesManager), +) -> GroupRoleResponse: + role = manager.delete(trans, role_id, group_id) + return group_role_to_model(trans, group_id, role) diff --git a/lib/galaxy/webapps/galaxy/api/group_users.py b/lib/galaxy/webapps/galaxy/api/group_users.py index 5fb77919771c..05756b45b231 100644 --- a/lib/galaxy/webapps/galaxy/api/group_users.py +++ b/lib/galaxy/webapps/galaxy/api/group_users.py @@ -34,79 +34,80 @@ def group_user_to_model(trans, group_id, user) -> GroupUserResponse: return GroupUserResponse(id=user.id, email=user.email, url=url) -@router.cbv -class FastAPIGroupUsers: - manager: GroupUsersManager = depends(GroupUsersManager) - - @router.get( - "/api/groups/{group_id}/users", - require_admin=True, - summary="Displays a collection (list) of groups.", - name="group_users", - ) - def index( - self, trans: ProvidesAppContext = DependsOnTrans, group_id: DecodedDatabaseIdField = GroupIDParam - ) -> GroupUserListResponse: - """ - GET /api/groups/{encoded_group_id}/users - Displays a collection (list) of groups. - """ - group_users = self.manager.index(trans, group_id) - return GroupUserListResponse(__root__=[group_user_to_model(trans, group_id, gr) for gr in group_users]) - - @router.get( - "/api/groups/{group_id}/user/{user_id}", - alias="/api/groups/{group_id}/users/{user_id}", - name="group_user", - require_admin=True, - summary="Displays information about a group user.", - ) - def show( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = GroupIDParam, - user_id: DecodedDatabaseIdField = UserIDParam, - ) -> GroupUserResponse: - """ - Displays information about a group user. - """ - user = self.manager.show(trans, user_id, group_id) - return group_user_to_model(trans, group_id, user) - - @router.put( - "/api/groups/{group_id}/users/{user_id}", - alias="/api/groups/{group_id}/user/{user_id}", - require_admin=True, - summary="Adds a user to a group", - ) - def update( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = GroupIDParam, - user_id: DecodedDatabaseIdField = UserIDParam, - ) -> GroupUserResponse: - """ - PUT /api/groups/{encoded_group_id}/users/{encoded_user_id} - Adds a user to a group - """ - user = self.manager.update(trans, user_id, group_id) - return group_user_to_model(trans, group_id, user) - - @router.delete( - "/api/groups/{group_id}/user/{user_id}", - alias="/api/groups/{group_id}/users/{user_id}", - require_admin=True, - summary="Removes a user from a group", - ) - def delete( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = GroupIDParam, - user_id: DecodedDatabaseIdField = UserIDParam, - ) -> GroupUserResponse: - """ - DELETE /api/groups/{encoded_group_id}/users/{encoded_user_id} - Removes a user from a group - """ - user = self.manager.delete(trans, user_id, group_id) - return group_user_to_model(trans, group_id, user) +@router.get( + "/api/groups/{group_id}/users", + require_admin=True, + summary="Displays a collection (list) of groups.", + name="group_users", +) +def index( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + manager: GroupUsersManager = depends(GroupUsersManager), +) -> GroupUserListResponse: + """ + GET /api/groups/{encoded_group_id}/users + Displays a collection (list) of groups. + """ + group_users = manager.index(trans, group_id) + return GroupUserListResponse(__root__=[group_user_to_model(trans, group_id, gr) for gr in group_users]) + + +@router.get( + "/api/groups/{group_id}/user/{user_id}", + alias="/api/groups/{group_id}/users/{user_id}", + name="group_user", + require_admin=True, + summary="Displays information about a group user.", +) +def show( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + user_id: DecodedDatabaseIdField = UserIDParam, + manager: GroupUsersManager = depends(GroupUsersManager), +) -> GroupUserResponse: + """ + Displays information about a group user. + """ + user = manager.show(trans, user_id, group_id) + return group_user_to_model(trans, group_id, user) + + +@router.put( + "/api/groups/{group_id}/users/{user_id}", + alias="/api/groups/{group_id}/user/{user_id}", + require_admin=True, + summary="Adds a user to a group", +) +def update( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + user_id: DecodedDatabaseIdField = UserIDParam, + manager: GroupUsersManager = depends(GroupUsersManager), +) -> GroupUserResponse: + """ + PUT /api/groups/{encoded_group_id}/users/{encoded_user_id} + Adds a user to a group + """ + user = manager.update(trans, user_id, group_id) + return group_user_to_model(trans, group_id, user) + + +@router.delete( + "/api/groups/{group_id}/user/{user_id}", + alias="/api/groups/{group_id}/users/{user_id}", + require_admin=True, + summary="Removes a user from a group", +) +def delete( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = GroupIDParam, + user_id: DecodedDatabaseIdField = UserIDParam, + manager: GroupUsersManager = depends(GroupUsersManager), +) -> GroupUserResponse: + """ + DELETE /api/groups/{encoded_group_id}/users/{encoded_user_id} + Removes a user from a group + """ + user = manager.delete(trans, user_id, group_id) + return group_user_to_model(trans, group_id, user) diff --git a/lib/galaxy/webapps/galaxy/api/groups.py b/lib/galaxy/webapps/galaxy/api/groups.py index c571983df48a..6a7c2f4e1133 100644 --- a/lib/galaxy/webapps/galaxy/api/groups.py +++ b/lib/galaxy/webapps/galaxy/api/groups.py @@ -27,70 +27,84 @@ router = Router(tags=["groups"]) -@router.cbv -class FastAPIGroups: - manager: GroupsManager = depends(GroupsManager) - - @router.get( - "/api/groups", - summary="Displays a collection (list) of groups.", - require_admin=True, - response_model_exclude_unset=True, - ) - def index( - self, - trans: ProvidesAppContext = DependsOnTrans, - ) -> GroupListResponse: - return self.manager.index(trans) - - @router.post( - "/api/groups", - summary="Creates a new group.", - require_admin=True, - response_model_exclude_unset=True, - ) - def create( - self, - trans: ProvidesAppContext = DependsOnTrans, - payload: GroupCreatePayload = Body(...), - ) -> GroupListResponse: - return self.manager.create(trans, payload) - - @router.get( - "/api/groups/{group_id}", - summary="Displays information about a group.", - require_admin=True, - name="show_group", - ) - def show( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = Path(...), - ) -> GroupResponse: - return self.manager.show(trans, group_id) - - @router.put( - "/api/groups/{group_id}", - summary="Modifies a group.", - require_admin=True, - response_model_exclude_unset=True, - ) - def update( - self, - trans: ProvidesAppContext = DependsOnTrans, - group_id: DecodedDatabaseIdField = Path(...), - payload: GroupCreatePayload = Body(...), - ) -> GroupResponse: - return self.manager.update(trans, group_id, payload) - - @router.delete("/api/groups/{group_id}", require_admin=True) - def delete(self, group_id: DecodedDatabaseIdField, trans: ProvidesAppContext = DependsOnTrans): - self.manager.delete(trans, group_id) - - @router.post("/api/groups/{group_id}/purge", require_admin=True) - def purge(self, group_id: DecodedDatabaseIdField, trans: ProvidesAppContext = DependsOnTrans): - self.manager.purge(trans, group_id) - - @router.post("/api/groups/{group_id}/undelete", require_admin=True) - def undelete(self, group_id: DecodedDatabaseIdField, trans: ProvidesAppContext = DependsOnTrans): - self.manager.undelete(trans, group_id) +@router.get( + "/api/groups", + summary="Displays a collection (list) of groups.", + require_admin=True, + response_model_exclude_unset=True, +) +def index( + trans: ProvidesAppContext = DependsOnTrans, + manager: GroupsManager = depends(GroupsManager), +) -> GroupListResponse: + return manager.index(trans) + + +@router.post( + "/api/groups", + summary="Creates a new group.", + require_admin=True, + response_model_exclude_unset=True, +) +def create( + trans: ProvidesAppContext = DependsOnTrans, + payload: GroupCreatePayload = Body(...), + manager: GroupsManager = depends(GroupsManager), +) -> GroupListResponse: + return manager.create(trans, payload) + + +@router.get( + "/api/groups/{group_id}", + summary="Displays information about a group.", + require_admin=True, + name="show_group", +) +def show( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = Path(...), + manager: GroupsManager = depends(GroupsManager), +) -> GroupResponse: + return manager.show(trans, group_id) + + +@router.put( + "/api/groups/{group_id}", + summary="Modifies a group.", + require_admin=True, + response_model_exclude_unset=True, +) +def update( + trans: ProvidesAppContext = DependsOnTrans, + group_id: DecodedDatabaseIdField = Path(...), + payload: GroupCreatePayload = Body(...), + manager: GroupsManager = depends(GroupsManager), +) -> GroupResponse: + return manager.update(trans, group_id, payload) + + +@router.delete("/api/groups/{group_id}", require_admin=True) +def delete( + group_id: DecodedDatabaseIdField, + trans: ProvidesAppContext = DependsOnTrans, + manager: GroupsManager = depends(GroupsManager), +): + manager.delete(trans, group_id) + + +@router.post("/api/groups/{group_id}/purge", require_admin=True) +def purge( + group_id: DecodedDatabaseIdField, + trans: ProvidesAppContext = DependsOnTrans, + manager: GroupsManager = depends(GroupsManager), +): + manager.purge(trans, group_id) + + +@router.post("/api/groups/{group_id}/undelete", require_admin=True) +def undelete( + group_id: DecodedDatabaseIdField, + trans: ProvidesAppContext = DependsOnTrans, + manager: GroupsManager = depends(GroupsManager), +): + manager.undelete(trans, group_id) diff --git a/lib/galaxy/webapps/galaxy/api/help.py b/lib/galaxy/webapps/galaxy/api/help.py index c9b8d599b8e2..3f76e362011b 100644 --- a/lib/galaxy/webapps/galaxy/api/help.py +++ b/lib/galaxy/webapps/galaxy/api/help.py @@ -16,20 +16,16 @@ router = Router(tags=["help"]) -@router.cbv -class HelpAPI: - service: HelpService = depends(HelpService) - - @router.get( - "/api/help/forum/search", - summary="Search the Galaxy Help forum.", - ) - def search_forum( - self, - query: Annotated[str, Query(description="Search query to use for searching the Galaxy Help forum.")], - ) -> HelpForumSearchResponse: - """Search the Galaxy Help forum using the Discourse API. - - **Note**: This endpoint is for **INTERNAL USE ONLY** and is not part of the public Galaxy API. - """ - return self.service.search_forum(query) +@router.get( + "/api/help/forum/search", + summary="Search the Galaxy Help forum.", +) +def search_forum( + query: Annotated[str, Query(description="Search query to use for searching the Galaxy Help forum.")], + service: HelpService = depends(HelpService), +) -> HelpForumSearchResponse: + """Search the Galaxy Help forum using the Discourse API. + + **Note**: This endpoint is for **INTERNAL USE ONLY** and is not part of the public Galaxy API. + """ + return service.search_forum(query) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index edeb029c4622..ff3ddff9a4c0 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -108,512 +108,533 @@ class CreateHistoryFormData(CreateHistoryPayload): """Uses Form data instead of JSON""" -@router.cbv -class FastAPIHistories: - service: HistoriesService = depends(HistoriesService) +@router.get( + "/api/histories", + summary="Returns histories for the current user.", +) +def index( + trans: ProvidesHistoryContext = DependsOnTrans, + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + serialization_params: SerializationParams = Depends(query_serialization_params), + all: Optional[bool] = AllHistoriesQueryParam, + deleted: Optional[bool] = Query( # This is for backward compatibility but looks redundant + default=False, + title="Deleted Only", + description="Whether to return only deleted items.", + deprecated=True, # Marked as deprecated as it seems just like '/api/histories/deleted' + ), + service: HistoriesService = depends(HistoriesService), +) -> List[AnyHistoryView]: + return service.index(trans, serialization_params, filter_query_params, deleted_only=deleted, all_histories=all) - @router.get( - "/api/histories", - summary="Returns histories for the current user.", - ) - def index( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - serialization_params: SerializationParams = Depends(query_serialization_params), - all: Optional[bool] = AllHistoriesQueryParam, - deleted: Optional[bool] = Query( # This is for backward compatibility but looks redundant - default=False, - title="Deleted Only", - description="Whether to return only deleted items.", - deprecated=True, # Marked as deprecated as it seems just like '/api/histories/deleted' - ), - ) -> List[AnyHistoryView]: - return self.service.index( - trans, serialization_params, filter_query_params, deleted_only=deleted, all_histories=all - ) - - @router.get( - "/api/histories/count", - summary="Returns number of histories for the current user.", - ) - def count( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> int: - return self.service.count(trans) - - @router.get( - "/api/histories/deleted", - summary="Returns deleted histories for the current user.", - ) - def index_deleted( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - serialization_params: SerializationParams = Depends(query_serialization_params), - all: Optional[bool] = AllHistoriesQueryParam, - ) -> List[AnyHistoryView]: - return self.service.index( - trans, serialization_params, filter_query_params, deleted_only=True, all_histories=all - ) - - @router.get( - "/api/histories/published", - summary="Return all histories that are published.", - ) - def published( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> List[AnyHistoryView]: - return self.service.published(trans, serialization_params, filter_query_params) - - @router.get( - "/api/histories/shared_with_me", - summary="Return all histories that are shared with the current user.", - ) - def shared_with_me( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> List[AnyHistoryView]: - return self.service.shared_with_me(trans, serialization_params, filter_query_params) - - @router.get( - "/api/histories/archived", - summary="Get a list of all archived histories for the current user.", - ) - def get_archived_histories( - self, - response: Response, - trans: ProvidesHistoryContext = DependsOnTrans, - serialization_params: SerializationParams = Depends(query_serialization_params), - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - ) -> List[AnyArchivedHistoryView]: - """Get a list of all archived histories for the current user. - - Archived histories are histories are not part of the active histories of the user but they can be accessed using this endpoint. - """ - archived_histories, total_matches = self.service.get_archived_histories( - trans, serialization_params, filter_query_params, include_total_matches=True - ) - response.headers["total_matches"] = str(total_matches) - return archived_histories - - @router.get( - "/api/histories/most_recently_used", - summary="Returns the most recently used history of the user.", - ) - def show_recent( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHistoryView: - return self.service.show(trans, serialization_params) - - @router.get( - "/api/histories/{history_id}", - name="history", - summary="Returns the history with the given ID.", - ) - def show( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHistoryView: - return self.service.show(trans, serialization_params, history_id) - - @router.post( - "/api/histories/{history_id}/prepare_store_download", - summary="Return a short term storage token to monitor download of the history.", - ) - def prepare_store_download( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: StoreExportPayload = Body(...), - ) -> AsyncFile: - return self.service.prepare_download( - trans, - history_id, - payload=payload, - ) - - @router.post( - "/api/histories/{history_id}/write_store", - summary="Prepare history for export-style download and write to supplied URI.", - ) - def write_store( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: WriteStoreToPayload = Body(...), - ) -> AsyncTaskResultSummary: - return self.service.write_store( - trans, - history_id, - payload=payload, - ) - - @router.get( - "/api/histories/{history_id}/citations", - summary="Return all the citations for the tools used to produce the datasets in the history.", - ) - def citations( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> List[Any]: - return self.service.citations(trans, history_id) - - @router.post( - "/api/histories", - summary="Creates a new history.", - ) - def create( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - payload: CreateHistoryPayload = Depends(CreateHistoryFormData.as_form), # type: ignore[attr-defined] - payload_as_json: Optional[Any] = Depends(try_get_request_body_as_json), - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> Union[JobImportHistoryResponse, AnyHistoryView]: - """The new history can also be copied form a existing history or imported from an archive or URL.""" - # This action needs to work both with json and x-www-form-urlencoded payloads. - # The way to support different content types on the same path operation is reading - # the request directly and parse it depending on the content type. - # We will assume x-www-form-urlencoded (payload) by default to deal with possible file uploads - # and if the content type is explicitly JSON, we will use payload_as_json instead. - # See https://github.com/tiangolo/fastapi/issues/990#issuecomment-639615888 - if payload_as_json: - payload = CreateHistoryPayload.parse_obj(payload_as_json) - return self.service.create(trans, payload, serialization_params) - - @router.delete( - "/api/histories/{history_id}", - summary="Marks the history with the given ID as deleted.", - ) - def delete( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - purge: bool = Query(default=False), - payload: Optional[DeleteHistoryPayload] = Body(default=None), - ) -> AnyHistoryView: - if payload: - purge = payload.purge - return self.service.delete(trans, history_id, serialization_params, purge) - - @router.post( - "/api/histories/deleted/{history_id}/undelete", - summary="Restores a deleted history with the given ID (that hasn't been purged).", - ) - def undelete( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHistoryView: - return self.service.undelete(trans, history_id, serialization_params) - - @router.put( - "/api/histories/{history_id}", - summary="Updates the values for the history with the given ID.", + +@router.get( + "/api/histories/count", + summary="Returns number of histories for the current user.", +) +def count( + trans: ProvidesHistoryContext = DependsOnTrans, + service: HistoriesService = depends(HistoriesService), +) -> int: + return service.count(trans) + + +@router.get( + "/api/histories/deleted", + summary="Returns deleted histories for the current user.", +) +def index_deleted( + trans: ProvidesHistoryContext = DependsOnTrans, + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + serialization_params: SerializationParams = Depends(query_serialization_params), + all: Optional[bool] = AllHistoriesQueryParam, + service: HistoriesService = depends(HistoriesService), +) -> List[AnyHistoryView]: + return service.index(trans, serialization_params, filter_query_params, deleted_only=True, all_histories=all) + + +@router.get( + "/api/histories/published", + summary="Return all histories that are published.", +) +def published( + trans: ProvidesHistoryContext = DependsOnTrans, + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> List[AnyHistoryView]: + return service.published(trans, serialization_params, filter_query_params) + + +@router.get( + "/api/histories/shared_with_me", + summary="Return all histories that are shared with the current user.", +) +def shared_with_me( + trans: ProvidesHistoryContext = DependsOnTrans, + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> List[AnyHistoryView]: + return service.shared_with_me(trans, serialization_params, filter_query_params) + + +@router.get( + "/api/histories/archived", + summary="Get a list of all archived histories for the current user.", +) +def get_archived_histories( + response: Response, + trans: ProvidesHistoryContext = DependsOnTrans, + serialization_params: SerializationParams = Depends(query_serialization_params), + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + service: HistoriesService = depends(HistoriesService), +) -> List[AnyArchivedHistoryView]: + """Get a list of all archived histories for the current user. + + Archived histories are histories are not part of the active histories of the user but they can be accessed using this endpoint. + """ + archived_histories, total_matches = service.get_archived_histories( + trans, serialization_params, filter_query_params, include_total_matches=True ) - def update( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: Any = Body( - ..., - description="Object containing any of the editable fields of the history.", - ), - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHistoryView: - return self.service.update(trans, history_id, payload, serialization_params) - - @router.post( - "/api/histories/from_store", - summary="Create histories from a model store.", + response.headers["total_matches"] = str(total_matches) + return archived_histories + + +@router.get( + "/api/histories/most_recently_used", + summary="Returns the most recently used history of the user.", +) +def show_recent( + trans: ProvidesHistoryContext = DependsOnTrans, + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + return service.show(trans, serialization_params) + + +@router.get( + "/api/histories/{history_id}", + name="history", + summary="Returns the history with the given ID.", +) +def show( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + return service.show(trans, serialization_params, history_id) + + +@router.post( + "/api/histories/{history_id}/prepare_store_download", + summary="Return a short term storage token to monitor download of the history.", +) +def prepare_store_download( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: StoreExportPayload = Body(...), + service: HistoriesService = depends(HistoriesService), +) -> AsyncFile: + return service.prepare_download( + trans, + history_id, + payload=payload, ) - def create_from_store( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - serialization_params: SerializationParams = Depends(query_serialization_params), - payload: CreateHistoryFromStore = Body(...), - ) -> AnyHistoryView: - return self.service.create_from_store(trans, payload, serialization_params) - - @router.post( - "/api/histories/from_store_async", - summary="Launch a task to create histories from a model store.", + + +@router.post( + "/api/histories/{history_id}/write_store", + summary="Prepare history for export-style download and write to supplied URI.", +) +def write_store( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: WriteStoreToPayload = Body(...), + service: HistoriesService = depends(HistoriesService), +) -> AsyncTaskResultSummary: + return service.write_store( + trans, + history_id, + payload=payload, ) - def create_from_store_async( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - payload: CreateHistoryFromStore = Body(...), - ) -> AsyncTaskResultSummary: - return self.service.create_from_store_async(trans, payload) - - @router.get( - "/api/histories/{history_id}/exports", - name="get_history_exports", - summary=("Get previous history exports."), - responses={ - 200: { - "description": "A list of history exports", - "content": { - "application/json": { - "schema": {"ref": "#/components/schemas/JobExportHistoryArchiveListResponse"}, - }, - ExportTaskListResponse.__accept_type__: { - "schema": {"ref": "#/components/schemas/ExportTaskListResponse"}, - }, + + +@router.get( + "/api/histories/{history_id}/citations", + summary="Return all the citations for the tools used to produce the datasets in the history.", +) +def citations( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> List[Any]: + return service.citations(trans, history_id) + + +@router.post( + "/api/histories", + summary="Creates a new history.", +) +def create( + trans: ProvidesHistoryContext = DependsOnTrans, + payload: CreateHistoryPayload = Depends(CreateHistoryFormData.as_form), # type: ignore[attr-defined] + payload_as_json: Optional[Any] = Depends(try_get_request_body_as_json), + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> Union[JobImportHistoryResponse, AnyHistoryView]: + """The new history can also be copied form a existing history or imported from an archive or URL.""" + # This action needs to work both with json and x-www-form-urlencoded payloads. + # The way to support different content types on the same path operation is reading + # the request directly and parse it depending on the content type. + # We will assume x-www-form-urlencoded (payload) by default to deal with possible file uploads + # and if the content type is explicitly JSON, we will use payload_as_json instead. + # See https://github.com/tiangolo/fastapi/issues/990#issuecomment-639615888 + if payload_as_json: + payload = CreateHistoryPayload.parse_obj(payload_as_json) + return service.create(trans, payload, serialization_params) + + +@router.delete( + "/api/histories/{history_id}", + summary="Marks the history with the given ID as deleted.", +) +def delete( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + purge: bool = Query(default=False), + payload: Optional[DeleteHistoryPayload] = Body(default=None), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + if payload: + purge = payload.purge + return service.delete(trans, history_id, serialization_params, purge) + + +@router.post( + "/api/histories/deleted/{history_id}/undelete", + summary="Restores a deleted history with the given ID (that hasn't been purged).", +) +def undelete( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + return service.undelete(trans, history_id, serialization_params) + + +@router.put( + "/api/histories/{history_id}", + summary="Updates the values for the history with the given ID.", +) +def update( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: Any = Body( + ..., + description="Object containing any of the editable fields of the history.", + ), + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + return service.update(trans, history_id, payload, serialization_params) + + +@router.post( + "/api/histories/from_store", + summary="Create histories from a model store.", +) +def create_from_store( + trans: ProvidesHistoryContext = DependsOnTrans, + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: CreateHistoryFromStore = Body(...), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + return service.create_from_store(trans, payload, serialization_params) + + +@router.post( + "/api/histories/from_store_async", + summary="Launch a task to create histories from a model store.", +) +def create_from_store_async( + trans: ProvidesHistoryContext = DependsOnTrans, + payload: CreateHistoryFromStore = Body(...), + service: HistoriesService = depends(HistoriesService), +) -> AsyncTaskResultSummary: + return service.create_from_store_async(trans, payload) + + +@router.get( + "/api/histories/{history_id}/exports", + name="get_history_exports", + summary=("Get previous history exports."), + responses={ + 200: { + "description": "A list of history exports", + "content": { + "application/json": { + "schema": {"ref": "#/components/schemas/JobExportHistoryArchiveListResponse"}, + }, + ExportTaskListResponse.__accept_type__: { + "schema": {"ref": "#/components/schemas/ExportTaskListResponse"}, }, }, }, - ) - def index_exports( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - limit: Optional[int] = LimitQueryParam, - offset: Optional[int] = OffsetQueryParam, - accept: str = Header(default="application/json", include_in_schema=False), - ) -> Union[JobExportHistoryArchiveListResponse, ExportTaskListResponse]: - """ - By default the legacy job-based history exports (jeha) are returned. - - Change the `accept` content type header to return the new task-based history exports. - """ - use_tasks = accept == ExportTaskListResponse.__accept_type__ - exports = self.service.index_exports(trans, history_id, use_tasks, limit, offset) - if use_tasks: - return ExportTaskListResponse(__root__=exports) - return JobExportHistoryArchiveListResponse(__root__=exports) - - @router.put( # PUT instead of POST because multiple requests should just result in one object being created. - "/api/histories/{history_id}/exports", - summary=("Start job (if needed) to create history export for corresponding history."), - responses={ - 200: { - "description": "Object containing url to fetch export from.", - }, - 202: { - "description": "The exported archive file is not ready yet.", - }, + }, +) +def index_exports( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + limit: Optional[int] = LimitQueryParam, + offset: Optional[int] = OffsetQueryParam, + accept: str = Header(default="application/json", include_in_schema=False), + service: HistoriesService = depends(HistoriesService), +) -> Union[JobExportHistoryArchiveListResponse, ExportTaskListResponse]: + """ + By default the legacy job-based history exports (jeha) are returned. + + Change the `accept` content type header to return the new task-based history exports. + """ + use_tasks = accept == ExportTaskListResponse.__accept_type__ + exports = service.index_exports(trans, history_id, use_tasks, limit, offset) + if use_tasks: + return ExportTaskListResponse(__root__=exports) + return JobExportHistoryArchiveListResponse(__root__=exports) + + +@router.put( # PUT instead of POST because multiple requests should just result in one object being created. + "/api/histories/{history_id}/exports", + summary=("Start job (if needed) to create history export for corresponding history."), + responses={ + 200: { + "description": "Object containing url to fetch export from.", }, - deprecated=True, - ) - def archive_export( - self, - response: Response, - trans=DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: Optional[ExportHistoryArchivePayload] = Body(None), - ) -> HistoryArchiveExportResult: - """This will start a job to create a history export archive. - - Calling this endpoint multiple times will return the 202 status code until the archive - has been completely generated and is ready to download. When ready, it will return - the 200 status code along with the download link information. - - If the history will be exported to a `directory_uri`, instead of returning the download - link information, the Job ID will be returned so it can be queried to determine when - the file has been written. - - **Deprecation notice**: Please use `/api/histories/{id}/prepare_store_download` or - `/api/histories/{id}/write_store` instead. - """ - export_result, ready = self.service.archive_export(trans, history_id, payload) - if not ready: - response.status_code = status.HTTP_202_ACCEPTED - return export_result - - @router.get( - "/api/histories/{history_id}/exports/{jeha_id}", - name="history_archive_download", - summary=("If ready and available, return raw contents of exported history as a downloadable archive."), - response_class=GalaxyFileResponse, - responses={ - 200: { - "description": "The archive file containing the History.", - } + 202: { + "description": "The exported archive file is not ready yet.", }, - deprecated=True, - ) - def archive_download( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - jeha_id: Union[DecodedDatabaseIdField, LatestLiteral] = JehaIDPathParam, - ): - """ - See ``PUT /api/histories/{id}/exports`` to initiate the creation - of the history export - when ready, that route will return 200 status - code (instead of 202) and this route can be used to download the archive. - - **Deprecation notice**: Please use `/api/histories/{id}/prepare_store_download` or - `/api/histories/{id}/write_store` instead. - """ - jeha = self.service.get_ready_history_export(trans, history_id, jeha_id) - media_type = self.service.get_archive_media_type(jeha) - file_path = self.service.get_archive_download_path(trans, jeha) - return GalaxyFileResponse( - path=file_path, - media_type=media_type, - filename=jeha.export_name, - ) - - @router.get( - "/api/histories/{history_id}/custom_builds_metadata", - summary="Returns meta data for custom builds.", - ) - def get_custom_builds_metadata( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> CustomBuildsMetadataResponse: - return self.service.get_custom_builds_metadata(trans, history_id) - - @router.post( - "/api/histories/{history_id}/archive", - summary="Archive a history.", - ) - def archive_history( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: Optional[ArchiveHistoryRequestPayload] = Body(default=None), - ) -> AnyArchivedHistoryView: - """Marks the given history as 'archived' and returns the history. - - Archiving a history will remove it from the list of active histories of the user but it will still be - accessible via the `/api/histories/{id}` or the `/api/histories/archived` endpoints. - - Associating an export record: - - - Optionally, an export record (containing information about a recent snapshot of the history) can be associated with the - archived history by providing an `archive_export_id` in the payload. The export record must belong to the history and - must be in the ready state. - - When associating an export record, the history can be purged after it has been archived using the `purge_history` flag. - - If the history is already archived, this endpoint will return a 409 Conflict error, indicating that the history is already archived. - If the history was not purged after it was archived, you can restore it using the `/api/histories/{id}/archive/restore` endpoint. - """ - return self.service.archive_history(trans, history_id, payload) - - @router.put( - "/api/histories/{history_id}/archive/restore", - summary="Restore an archived history.", - ) - def restore_archived_history( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - force: Optional[bool] = Query( - default=None, - description="If true, the history will be un-archived even if it has an associated archive export record and was purged.", - ), - ) -> AnyHistoryView: - """Restores an archived history and returns it. - - Restoring an archived history will add it back to the list of active histories of the user (unless it was purged). - - **Warning**: Please note that histories that are associated with an archive export might be purged after export, so un-archiving them - will not restore the datasets that were in the history before it was archived. You will need to import back the archive export - record to restore the history and its datasets as a new copy. See `/api/histories/from_store_async` for more information. - """ - return self.service.restore_archived_history(trans, history_id, force) - - @router.get( - "/api/histories/{history_id}/sharing", - summary="Get the current sharing status of the given item.", - ) - def sharing( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> SharingStatus: - """Return the sharing status of the item.""" - return self.service.shareable_service.sharing(trans, history_id) - - @router.put( - "/api/histories/{history_id}/enable_link_access", - summary="Makes this item accessible by a URL link.", - ) - def enable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> SharingStatus: - """Makes this item accessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.enable_link_access(trans, history_id) - - @router.put( - "/api/histories/{history_id}/disable_link_access", - summary="Makes this item inaccessible by a URL link.", - ) - def disable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> SharingStatus: - """Makes this item inaccessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.disable_link_access(trans, history_id) - - @router.put( - "/api/histories/{history_id}/publish", - summary="Makes this item public and accessible by a URL link.", - ) - def publish( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> SharingStatus: - """Makes this item publicly available by a URL link and return the current sharing status.""" - return self.service.shareable_service.publish(trans, history_id) - - @router.put( - "/api/histories/{history_id}/unpublish", - summary="Removes this item from the published list.", - ) - def unpublish( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - ) -> SharingStatus: - """Removes this item from the published list and return the current sharing status.""" - return self.service.shareable_service.unpublish(trans, history_id) - - @router.put( - "/api/histories/{history_id}/share_with_users", - summary="Share this item with specific users.", - ) - def share_with_users( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: ShareWithPayload = Body(...), - ) -> ShareWithStatus: - """Shares this item with specific users and return the current sharing status.""" - return self.service.shareable_service.share_with_users(trans, history_id, payload) - - @router.put( - "/api/histories/{history_id}/slug", - summary="Set a new slug for this shared item.", - status_code=status.HTTP_204_NO_CONTENT, + }, + deprecated=True, +) +def archive_export( + response: Response, + trans=DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: Optional[ExportHistoryArchivePayload] = Body(None), + service: HistoriesService = depends(HistoriesService), +) -> HistoryArchiveExportResult: + """This will start a job to create a history export archive. + + Calling this endpoint multiple times will return the 202 status code until the archive + has been completely generated and is ready to download. When ready, it will return + the 200 status code along with the download link information. + + If the history will be exported to a `directory_uri`, instead of returning the download + link information, the Job ID will be returned so it can be queried to determine when + the file has been written. + + **Deprecation notice**: Please use `/api/histories/{id}/prepare_store_download` or + `/api/histories/{id}/write_store` instead. + """ + export_result, ready = service.archive_export(trans, history_id, payload) + if not ready: + response.status_code = status.HTTP_202_ACCEPTED + return export_result + + +@router.get( + "/api/histories/{history_id}/exports/{jeha_id}", + name="history_archive_download", + summary=("If ready and available, return raw contents of exported history as a downloadable archive."), + response_class=GalaxyFileResponse, + responses={ + 200: { + "description": "The archive file containing the History.", + } + }, + deprecated=True, +) +def archive_download( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + jeha_id: Union[DecodedDatabaseIdField, LatestLiteral] = JehaIDPathParam, + service: HistoriesService = depends(HistoriesService), +): + """ + See ``PUT /api/histories/{id}/exports`` to initiate the creation + of the history export - when ready, that route will return 200 status + code (instead of 202) and this route can be used to download the archive. + + **Deprecation notice**: Please use `/api/histories/{id}/prepare_store_download` or + `/api/histories/{id}/write_store` instead. + """ + jeha = service.get_ready_history_export(trans, history_id, jeha_id) + media_type = service.get_archive_media_type(jeha) + file_path = service.get_archive_download_path(trans, jeha) + return GalaxyFileResponse( + path=file_path, + media_type=media_type, + filename=jeha.export_name, ) - def set_slug( - self, - trans: ProvidesUserContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - payload: SetSlugPayload = Body(...), - ): - """Sets a new slug to access this item by URL. The new slug must be unique.""" - self.service.shareable_service.set_slug(trans, history_id, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/api/histories/{history_id}/custom_builds_metadata", + summary="Returns meta data for custom builds.", +) +def get_custom_builds_metadata( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> CustomBuildsMetadataResponse: + return service.get_custom_builds_metadata(trans, history_id) + + +@router.post( + "/api/histories/{history_id}/archive", + summary="Archive a history.", +) +def archive_history( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: Optional[ArchiveHistoryRequestPayload] = Body(default=None), + service: HistoriesService = depends(HistoriesService), +) -> AnyArchivedHistoryView: + """Marks the given history as 'archived' and returns the history. + + Archiving a history will remove it from the list of active histories of the user but it will still be + accessible via the `/api/histories/{id}` or the `/api/histories/archived` endpoints. + + Associating an export record: + + - Optionally, an export record (containing information about a recent snapshot of the history) can be associated with the + archived history by providing an `archive_export_id` in the payload. The export record must belong to the history and + must be in the ready state. + - When associating an export record, the history can be purged after it has been archived using the `purge_history` flag. + + If the history is already archived, this endpoint will return a 409 Conflict error, indicating that the history is already archived. + If the history was not purged after it was archived, you can restore it using the `/api/histories/{id}/archive/restore` endpoint. + """ + return service.archive_history(trans, history_id, payload) + + +@router.put( + "/api/histories/{history_id}/archive/restore", + summary="Restore an archived history.", +) +def restore_archived_history( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + force: Optional[bool] = Query( + default=None, + description="If true, the history will be un-archived even if it has an associated archive export record and was purged.", + ), + service: HistoriesService = depends(HistoriesService), +) -> AnyHistoryView: + """Restores an archived history and returns it. + + Restoring an archived history will add it back to the list of active histories of the user (unless it was purged). + + **Warning**: Please note that histories that are associated with an archive export might be purged after export, so un-archiving them + will not restore the datasets that were in the history before it was archived. You will need to import back the archive export + record to restore the history and its datasets as a new copy. See `/api/histories/from_store_async` for more information. + """ + return service.restore_archived_history(trans, history_id, force) + + +@router.get( + "/api/histories/{history_id}/sharing", + summary="Get the current sharing status of the given item.", +) +def sharing( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> SharingStatus: + """Return the sharing status of the item.""" + return service.shareable_service.sharing(trans, history_id) + + +@router.put( + "/api/histories/{history_id}/enable_link_access", + summary="Makes this item accessible by a URL link.", +) +def enable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> SharingStatus: + """Makes this item accessible by a URL link and return the current sharing status.""" + return service.shareable_service.enable_link_access(trans, history_id) + + +@router.put( + "/api/histories/{history_id}/disable_link_access", + summary="Makes this item inaccessible by a URL link.", +) +def disable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> SharingStatus: + """Makes this item inaccessible by a URL link and return the current sharing status.""" + return service.shareable_service.disable_link_access(trans, history_id) + + +@router.put( + "/api/histories/{history_id}/publish", + summary="Makes this item public and accessible by a URL link.", +) +def publish( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> SharingStatus: + """Makes this item publicly available by a URL link and return the current sharing status.""" + return service.shareable_service.publish(trans, history_id) + + +@router.put( + "/api/histories/{history_id}/unpublish", + summary="Removes this item from the published list.", +) +def unpublish( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + service: HistoriesService = depends(HistoriesService), +) -> SharingStatus: + """Removes this item from the published list and return the current sharing status.""" + return service.shareable_service.unpublish(trans, history_id) + + +@router.put( + "/api/histories/{history_id}/share_with_users", + summary="Share this item with specific users.", +) +def share_with_users( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: ShareWithPayload = Body(...), + service: HistoriesService = depends(HistoriesService), +) -> ShareWithStatus: + """Shares this item with specific users and return the current sharing status.""" + return service.shareable_service.share_with_users(trans, history_id, payload) + + +@router.put( + "/api/histories/{history_id}/slug", + summary="Set a new slug for this shared item.", + status_code=status.HTTP_204_NO_CONTENT, +) +def set_slug( + trans: ProvidesUserContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + payload: SetSlugPayload = Body(...), + service: HistoriesService = depends(HistoriesService), +): + """Sets a new slug to access this item by URL. The new slug must be unique.""" + service.shareable_service.set_slug(trans, history_id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 03ab15a0638f..aaefa3989563 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -382,646 +382,669 @@ def parse_index_jobs_summary_params( return HistoryContentsIndexJobsSummaryParams(ids=util.listify(ids), types=util.listify(types)) -@router.cbv -class FastAPIHistoryContents: - service: HistoriesContentsService = depends(HistoriesContentsService) - - @router.get( - "/api/histories/{history_id}/contents/{type}s", - summary="Returns the contents of the given history filtered by type.", - operation_id="history_contents__index_typed", +@router.get( + "/api/histories/{history_id}/contents/{type}s", + summary="Returns the contents of the given history filtered by type.", + operation_id="history_contents__index_typed", +) +def index_typed( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + index_params: HistoryContentsIndexParams = Depends(get_index_query_params), + type: HistoryContentType = ContentTypePathParam, + legacy_params: LegacyHistoryContentsIndexParams = Depends(get_legacy_index_query_params), + serialization_params: SerializationParams = Depends(query_serialization_params), + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + accept: str = Header(default="application/json", include_in_schema=False), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> Union[HistoryContentsResult, HistoryContentsWithStatsResult]: + """ + Return a list of either `HDA`/`HDCA` data for the history with the given ``ID``. + + - The contents can be filtered and queried using the appropriate parameters. + - The amount of information returned for each item can be customized. + + **Note**: Anonymous users are allowed to get their current history contents. + """ + legacy_params.types = [type] + items = service.index( + trans, + history_id, + index_params, + legacy_params, + serialization_params, + filter_query_params, + accept, ) - def index_typed( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - index_params: HistoryContentsIndexParams = Depends(get_index_query_params), - type: HistoryContentType = ContentTypePathParam, - legacy_params: LegacyHistoryContentsIndexParams = Depends(get_legacy_index_query_params), - serialization_params: SerializationParams = Depends(query_serialization_params), - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - accept: str = Header(default="application/json", include_in_schema=False), - ) -> Union[HistoryContentsResult, HistoryContentsWithStatsResult]: - """ - Return a list of either `HDA`/`HDCA` data for the history with the given ``ID``. - - - The contents can be filtered and queried using the appropriate parameters. - - The amount of information returned for each item can be customized. - - **Note**: Anonymous users are allowed to get their current history contents. - """ - legacy_params.types = [type] - items = self.service.index( - trans, - history_id, - index_params, - legacy_params, - serialization_params, - filter_query_params, - accept, - ) - return items - - @router.get( - "/api/histories/{history_id}/contents", - name="history_contents", - summary="Returns the contents of the given history.", - responses={ - 200: { - "description": ("The contents of the history that match the query."), - "content": { - "application/json": { - "schema": { # HistoryContentsResult.schema(), - "ref": "#/components/schemas/HistoryContentsResult" - }, - }, - HistoryContentsWithStatsResult.__accept_type__: { - "schema": { # HistoryContentsWithStatsResult.schema(), - "ref": "#/components/schemas/HistoryContentsWithStatsResult" - }, + return items + + +@router.get( + "/api/histories/{history_id}/contents", + name="history_contents", + summary="Returns the contents of the given history.", + responses={ + 200: { + "description": ("The contents of the history that match the query."), + "content": { + "application/json": { + "schema": {"ref": "#/components/schemas/HistoryContentsResult"}, # HistoryContentsResult.schema(), + }, + HistoryContentsWithStatsResult.__accept_type__: { + "schema": { # HistoryContentsWithStatsResult.schema(), + "ref": "#/components/schemas/HistoryContentsWithStatsResult" }, }, }, }, - operation_id="history_contents__index", + }, + operation_id="history_contents__index", +) +def index( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + index_params: HistoryContentsIndexParams = Depends(get_index_query_params), + type: Optional[str] = Query(default=None, include_in_schema=False, deprecated=True), + legacy_params: LegacyHistoryContentsIndexParams = Depends(get_legacy_index_query_params), + serialization_params: SerializationParams = Depends(query_serialization_params), + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + accept: str = Header(default="application/json", include_in_schema=False), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> Union[HistoryContentsResult, HistoryContentsWithStatsResult]: + """ + Return a list of `HDA`/`HDCA` data for the history with the given ``ID``. + + - The contents can be filtered and queried using the appropriate parameters. + - The amount of information returned for each item can be customized. + + **Note**: Anonymous users are allowed to get their current history contents. + """ + if type is not None: + legacy_params.types = parse_content_types(type) + items = service.index( + trans, + history_id, + index_params, + legacy_params, + serialization_params, + filter_query_params, + accept, ) - def index( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - index_params: HistoryContentsIndexParams = Depends(get_index_query_params), - type: Optional[str] = Query(default=None, include_in_schema=False, deprecated=True), - legacy_params: LegacyHistoryContentsIndexParams = Depends(get_legacy_index_query_params), - serialization_params: SerializationParams = Depends(query_serialization_params), - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - accept: str = Header(default="application/json", include_in_schema=False), - ) -> Union[HistoryContentsResult, HistoryContentsWithStatsResult]: - """ - Return a list of `HDA`/`HDCA` data for the history with the given ``ID``. - - - The contents can be filtered and queried using the appropriate parameters. - - The amount of information returned for each item can be customized. - - **Note**: Anonymous users are allowed to get their current history contents. - """ - if type is not None: - legacy_params.types = parse_content_types(type) - items = self.service.index( - trans, - history_id, - index_params, - legacy_params, - serialization_params, - filter_query_params, - accept, - ) - return items + return items - @router.get( - "/api/histories/{history_id}/contents/{type}s/{id}/jobs_summary", - summary="Return detailed information about an `HDA` or `HDCAs` jobs.", - ) - def show_jobs_summary( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypePathParam, - ) -> AnyJobStateSummary: - """Return detailed information about an `HDA` or `HDCAs` jobs. - - **Warning**: We allow anyone to fetch job state information about any object they - can guess an encoded ID for - it isn't considered protected data. This keeps - polling IDs as part of state calculation for large histories and collections as - efficient as possible. - """ - return self.service.show_jobs_summary(trans, id, contents_type=type) - - @router.get( - "/api/histories/{history_id}/contents/{type}s/{id}", - name="history_content_typed", - summary="Return detailed information about a specific HDA or HDCA with the given `ID` within a history.", - operation_id="history_contents__show", - ) - def show( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypePathParam, - fuzzy_count: Optional[int] = FuzzyCountQueryParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHistoryContentItem: - """ - Return detailed information about an `HDA` or `HDCA` within a history. - - **Note**: Anonymous users are allowed to get their current history contents. - """ - return self.service.show( - trans, - id=id, - serialization_params=serialization_params, - contents_type=type, - fuzzy_count=fuzzy_count, - ) - @router.get( - "/api/histories/{history_id}/contents/{id}", - name="history_content", - summary="Return detailed information about an HDA within a history. ``/api/histories/{history_id}/contents/{type}s/{id}`` should be used instead.", - deprecated=True, - operation_id="history_contents__show_legacy", +@router.get( + "/api/histories/{history_id}/contents/{type}s/{id}/jobs_summary", + summary="Return detailed information about an `HDA` or `HDCAs` jobs.", +) +def show_jobs_summary( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypePathParam, + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AnyJobStateSummary: + """Return detailed information about an `HDA` or `HDCAs` jobs. + + **Warning**: We allow anyone to fetch job state information about any object they + can guess an encoded ID for - it isn't considered protected data. This keeps + polling IDs as part of state calculation for large histories and collections as + efficient as possible. + """ + return service.show_jobs_summary(trans, id, contents_type=type) + + +@router.get( + "/api/histories/{history_id}/contents/{type}s/{id}", + name="history_content_typed", + summary="Return detailed information about a specific HDA or HDCA with the given `ID` within a history.", + operation_id="history_contents__show", +) +def show( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypePathParam, + fuzzy_count: Optional[int] = FuzzyCountQueryParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AnyHistoryContentItem: + """ + Return detailed information about an `HDA` or `HDCA` within a history. + + **Note**: Anonymous users are allowed to get their current history contents. + """ + return service.show( + trans, + id=id, + serialization_params=serialization_params, + contents_type=type, + fuzzy_count=fuzzy_count, ) - def show_legacy( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - type: HistoryContentType = ContentTypeQueryParam(default=HistoryContentType.dataset), - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - fuzzy_count: Optional[int] = FuzzyCountQueryParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - ) -> AnyHistoryContentItem: - """ - Return detailed information about an `HDA` or `HDCA` within a history. - - **Note**: Anonymous users are allowed to get their current history contents. - """ - return self.service.show( - trans, - id=id, - serialization_params=serialization_params, - contents_type=type, - fuzzy_count=fuzzy_count, - ) - @router.post( - "/api/histories/{history_id}/contents/{type}s/{id}/prepare_store_download", - summary="Prepare a dataset or dataset collection for export-style download.", - ) - def prepare_store_download( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypePathParam, - payload: StoreExportPayload = Body(...), - ) -> AsyncFile: - return self.service.prepare_store_download( - trans, - id, - contents_type=type, - payload=payload, - ) - @router.post( - "/api/histories/{history_id}/contents/{type}s/{id}/write_store", - summary="Prepare a dataset or dataset collection for export-style download and write to supplied URI.", +@router.get( + "/api/histories/{history_id}/contents/{id}", + name="history_content", + summary="Return detailed information about an HDA within a history. ``/api/histories/{history_id}/contents/{type}s/{id}`` should be used instead.", + deprecated=True, + operation_id="history_contents__show_legacy", +) +def show_legacy( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + type: HistoryContentType = ContentTypeQueryParam(default=HistoryContentType.dataset), + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + fuzzy_count: Optional[int] = FuzzyCountQueryParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AnyHistoryContentItem: + """ + Return detailed information about an `HDA` or `HDCA` within a history. + + **Note**: Anonymous users are allowed to get their current history contents. + """ + return service.show( + trans, + id=id, + serialization_params=serialization_params, + contents_type=type, + fuzzy_count=fuzzy_count, ) - def write_store( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypePathParam, - payload: WriteStoreToPayload = Body(...), - ) -> AsyncTaskResultSummary: - return self.service.write_store( - trans, - id, - contents_type=type, - payload=payload, - ) - @router.get( - "/api/histories/{history_id}/jobs_summary", - summary="Return job state summary info for jobs, implicit groups jobs for collections or workflow invocations.", - ) - def index_jobs_summary( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - params: HistoryContentsIndexJobsSummaryParams = Depends(get_index_jobs_summary_params), - ) -> List[AnyJobStateSummary]: - """Return job state summary info for jobs, implicit groups jobs for collections or workflow invocations. - - **Warning**: We allow anyone to fetch job state information about any object they - can guess an encoded ID for - it isn't considered protected data. This keeps - polling IDs as part of state calculation for large histories and collections as - efficient as possible. - """ - return self.service.index_jobs_summary(trans, params) - - @router.get( - "/api/histories/{history_id}/contents/dataset_collections/{id}/download", - summary="Download the content of a dataset collection as a `zip` archive.", - response_class=StreamingResponse, - operation_id="history_contents__download_collection", - ) - def download_dataset_collection_history_content( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: Optional[DecodedDatabaseIdField] = Path( - description="The encoded database identifier of the History.", - ), - id: DecodedDatabaseIdField = HistoryHDCAIDPathParam, - ): - """Download the content of a history dataset collection as a `zip` archive - while maintaining approximate collection structure. - """ - archive = self.service.get_dataset_collection_archive_for_download(trans, id) - return StreamingResponse(archive.response(), headers=archive.get_headers()) - - @router.get( - "/api/dataset_collections/{id}/download", - summary="Download the content of a dataset collection as a `zip` archive.", - response_class=StreamingResponse, - tags=["dataset collections"], - operation_id="dataset_collections__download", + +@router.post( + "/api/histories/{history_id}/contents/{type}s/{id}/prepare_store_download", + summary="Prepare a dataset or dataset collection for export-style download.", +) +def prepare_store_download( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypePathParam, + payload: StoreExportPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AsyncFile: + return service.prepare_store_download( + trans, + id, + contents_type=type, + payload=payload, ) - def download_dataset_collection( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - id: DecodedDatabaseIdField = HistoryHDCAIDPathParam, - ): - """Download the content of a history dataset collection as a `zip` archive - while maintaining approximate collection structure. - """ - archive = self.service.get_dataset_collection_archive_for_download(trans, id) - return StreamingResponse(archive.response(), headers=archive.get_headers()) - - @router.post( - "/api/histories/{history_id}/contents/dataset_collections/{id}/prepare_download", - summary="Prepare an short term storage object that the collection will be downloaded to.", - include_in_schema=False, + + +@router.post( + "/api/histories/{history_id}/contents/{type}s/{id}/write_store", + summary="Prepare a dataset or dataset collection for export-style download and write to supplied URI.", +) +def write_store( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypePathParam, + payload: WriteStoreToPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AsyncTaskResultSummary: + return service.write_store( + trans, + id, + contents_type=type, + payload=payload, ) - @router.post( - "/api/dataset_collections/{id}/prepare_download", - summary="Prepare an short term storage object that the collection will be downloaded to.", - responses={ - 200: { - "description": "Short term storage reference for async monitoring of this download.", - }, - 501: {"description": "Required asynchronous tasks required for this operation not available."}, + + +@router.get( + "/api/histories/{history_id}/jobs_summary", + summary="Return job state summary info for jobs, implicit groups jobs for collections or workflow invocations.", +) +def index_jobs_summary( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + params: HistoryContentsIndexJobsSummaryParams = Depends(get_index_jobs_summary_params), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> List[AnyJobStateSummary]: + """Return job state summary info for jobs, implicit groups jobs for collections or workflow invocations. + + **Warning**: We allow anyone to fetch job state information about any object they + can guess an encoded ID for - it isn't considered protected data. This keeps + polling IDs as part of state calculation for large histories and collections as + efficient as possible. + """ + return service.index_jobs_summary(trans, params) + + +@router.get( + "/api/histories/{history_id}/contents/dataset_collections/{id}/download", + summary="Download the content of a dataset collection as a `zip` archive.", + response_class=StreamingResponse, + operation_id="history_contents__download_collection", +) +def download_dataset_collection_history_content( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: Optional[DecodedDatabaseIdField] = Path( + description="The encoded database identifier of the History.", + ), + id: DecodedDatabaseIdField = HistoryHDCAIDPathParam, + service: HistoriesContentsService = depends(HistoriesContentsService), +): + """Download the content of a history dataset collection as a `zip` archive + while maintaining approximate collection structure. + """ + archive = service.get_dataset_collection_archive_for_download(trans, id) + return StreamingResponse(archive.response(), headers=archive.get_headers()) + + +@router.get( + "/api/dataset_collections/{id}/download", + summary="Download the content of a dataset collection as a `zip` archive.", + response_class=StreamingResponse, + tags=["dataset collections"], + operation_id="dataset_collections__download", +) +def download_dataset_collection( + trans: ProvidesHistoryContext = DependsOnTrans, + id: DecodedDatabaseIdField = HistoryHDCAIDPathParam, + service: HistoriesContentsService = depends(HistoriesContentsService), +): + """Download the content of a history dataset collection as a `zip` archive + while maintaining approximate collection structure. + """ + archive = service.get_dataset_collection_archive_for_download(trans, id) + return StreamingResponse(archive.response(), headers=archive.get_headers()) + + +@router.post( + "/api/histories/{history_id}/contents/dataset_collections/{id}/prepare_download", + summary="Prepare an short term storage object that the collection will be downloaded to.", + include_in_schema=False, +) +@router.post( + "/api/dataset_collections/{id}/prepare_download", + summary="Prepare an short term storage object that the collection will be downloaded to.", + responses={ + 200: { + "description": "Short term storage reference for async monitoring of this download.", }, - tags=["dataset collections"], - ) - def prepare_collection_download( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - id: DecodedDatabaseIdField = HistoryHDCAIDPathParam, - ) -> AsyncFile: - """The history dataset collection will be written as a `zip` archive to the - returned short term storage object. Progress tracking this file's creation - can be tracked with the short_term_storage API. - """ - return self.service.prepare_collection_download(trans, id) - - @router.post( - "/api/histories/{history_id}/contents/{type}s", - summary="Create a new `HDA` or `HDCA` in the given History.", - operation_id="history_contents__create_typed", - ) - def create_typed( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - type: HistoryContentType = ContentTypePathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - payload: CreateHistoryContentPayload = Body(...), - ) -> Union[AnyHistoryContentItem, List[AnyHistoryContentItem]]: - """Create a new `HDA` or `HDCA` in the given History.""" - return self._create(trans, history_id, type, serialization_params, payload) - - @router.post( - "/api/histories/{history_id}/contents", - summary="Create a new `HDA` or `HDCA` in the given History.", - deprecated=True, - operation_id="history_contents__create", - ) - def create( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - type: Optional[HistoryContentType] = ContentTypeQueryParam(default=None), - serialization_params: SerializationParams = Depends(query_serialization_params), - payload: CreateHistoryContentPayload = Body(...), - ) -> Union[AnyHistoryContentItem, List[AnyHistoryContentItem]]: - """Create a new `HDA` or `HDCA` in the given History.""" - return self._create(trans, history_id, type, serialization_params, payload) - - def _create( - self, - trans: ProvidesHistoryContext, - history_id: DecodedDatabaseIdField, - type: Optional[HistoryContentType], - serialization_params: SerializationParams, - payload: CreateHistoryContentPayload, - ) -> Union[AnyHistoryContentItem, List[AnyHistoryContentItem]]: - """Create a new `HDA` or `HDCA` in the given History.""" - payload.type = type or payload.type - return self.service.create(trans, history_id, payload, serialization_params) - - @router.put( - "/api/histories/{history_id}/contents/{dataset_id}/permissions", - summary="Set permissions of the given history dataset to the given role ids.", - ) - def update_permissions( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - dataset_id: DecodedDatabaseIdField = HistoryItemIDPathParam, - # Using a generic Dict here as an attempt on supporting multiple aliases for the permissions params. - payload: Dict[str, Any] = Body( - default=..., - example=UpdateDatasetPermissionsPayload(), - ), - ) -> DatasetAssociationRoles: - """Set permissions of the given history dataset to the given role ids.""" - update_payload = get_update_permission_payload(payload) - return self.service.update_permissions(trans, dataset_id, update_payload) - - @router.put( - "/api/histories/{history_id}/contents", - summary="Batch update specific properties of a set items contained in the given History.", - ) - def update_batch( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - payload: UpdateHistoryContentsBatchPayload = Body(...), - ) -> HistoryContentsResult: - """Batch update specific properties of a set items contained in the given History. - - If you provide an invalid/unknown property key the request will not fail, but no changes - will be made to the items. - """ - result = self.service.update_batch(trans, history_id, payload, serialization_params) - return HistoryContentsResult.construct(__root__=result) - - @router.put( - "/api/histories/{history_id}/contents/bulk", - summary="Executes an operation on a set of items contained in the given History.", - ) - def bulk_operation( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - filter_query_params: ValueFilterQueryParams = Depends(get_value_filter_query_params), - payload: HistoryContentBulkOperationPayload = Body(...), - ) -> HistoryContentBulkOperationResult: - """Executes an operation on a set of items contained in the given History. - - The items to be processed can be explicitly set or determined by a dynamic query. - """ - return self.service.bulk_operation(trans, history_id, filter_query_params, payload) - - @router.put( - "/api/histories/{history_id}/contents/{id}/validate", - summary="Validates the metadata associated with a dataset within a History.", - ) - def validate( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - ) -> dict: # TODO: define a response? - """Validates the metadata associated with a dataset within a History.""" - return self.service.validate(trans, history_id, id) - - @router.put( - "/api/histories/{history_id}/contents/{type}s/{id}", - summary="Updates the values for the history content item with the given ``ID`` and path specified type.", - operation_id="history_contents__update_typed", - ) - def update_typed( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypePathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - payload: UpdateHistoryContentsPayload = Body(...), - ) -> AnyHistoryContentItem: - """Updates the values for the history content item with the given ``ID``.""" - return self.service.update( - trans, history_id, id, payload.dict(exclude_unset=True), serialization_params, contents_type=type - ) + 501: {"description": "Required asynchronous tasks required for this operation not available."}, + }, + tags=["dataset collections"], +) +def prepare_collection_download( + trans: ProvidesHistoryContext = DependsOnTrans, + id: DecodedDatabaseIdField = HistoryHDCAIDPathParam, + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AsyncFile: + """The history dataset collection will be written as a `zip` archive to the + returned short term storage object. Progress tracking this file's creation + can be tracked with the short_term_storage API. + """ + return service.prepare_collection_download(trans, id) + + +@router.post( + "/api/histories/{history_id}/contents/{type}s", + summary="Create a new `HDA` or `HDCA` in the given History.", + operation_id="history_contents__create_typed", +) +def create_typed( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + type: HistoryContentType = ContentTypePathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: CreateHistoryContentPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> Union[AnyHistoryContentItem, List[AnyHistoryContentItem]]: + """Create a new `HDA` or `HDCA` in the given History.""" + return _create(trans, history_id, type, serialization_params, payload, service) + + +@router.post( + "/api/histories/{history_id}/contents", + summary="Create a new `HDA` or `HDCA` in the given History.", + deprecated=True, + operation_id="history_contents__create", +) +def create( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + type: Optional[HistoryContentType] = ContentTypeQueryParam(default=None), + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: CreateHistoryContentPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> Union[AnyHistoryContentItem, List[AnyHistoryContentItem]]: + """Create a new `HDA` or `HDCA` in the given History.""" + return _create(trans, history_id, type, serialization_params, payload, service) + + +@router.put( + "/api/histories/{history_id}/contents/{dataset_id}/permissions", + summary="Set permissions of the given history dataset to the given role ids.", +) +def update_permissions( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + dataset_id: DecodedDatabaseIdField = HistoryItemIDPathParam, + # Using a generic Dict here as an attempt on supporting multiple aliases for the permissions params. + payload: Dict[str, Any] = Body( + default=..., + example=UpdateDatasetPermissionsPayload(), + ), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> DatasetAssociationRoles: + """Set permissions of the given history dataset to the given role ids.""" + update_payload = get_update_permission_payload(payload) + return service.update_permissions(trans, dataset_id, update_payload) - @router.put( - "/api/histories/{history_id}/contents/{id}", - summary="Updates the values for the history content item with the given ``ID`` and query specified type. ``/api/histories/{history_id}/contents/{type}s/{id}`` should be used instead.", - deprecated=True, - operation_id="history_contents__update_legacy", - ) - def update_legacy( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypeQueryParam(default=HistoryContentType.dataset), - serialization_params: SerializationParams = Depends(query_serialization_params), - payload: UpdateHistoryContentsPayload = Body(...), - ) -> AnyHistoryContentItem: - """Updates the values for the history content item with the given ``ID``.""" - return self.service.update( - trans, history_id, id, payload.dict(exclude_unset=True), serialization_params, contents_type=type - ) - @router.delete( - "/api/histories/{history_id}/contents/{type}s/{id}", - summary="Delete the history content with the given ``ID`` and path specified type.", - responses=CONTENT_DELETE_RESPONSES, - operation_id="history_contents__delete_typed", +@router.put( + "/api/histories/{history_id}/contents", + summary="Batch update specific properties of a set items contained in the given History.", +) +def update_batch( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: UpdateHistoryContentsBatchPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> HistoryContentsResult: + """Batch update specific properties of a set items contained in the given History. + + If you provide an invalid/unknown property key the request will not fail, but no changes + will be made to the items. + """ + result = service.update_batch(trans, history_id, payload, serialization_params) + return HistoryContentsResult.construct(__root__=result) + + +@router.put( + "/api/histories/{history_id}/contents/bulk", + summary="Executes an operation on a set of items contained in the given History.", +) +def bulk_operation( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + filter_query_params: ValueFilterQueryParams = Depends(get_value_filter_query_params), + payload: HistoryContentBulkOperationPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> HistoryContentBulkOperationResult: + """Executes an operation on a set of items contained in the given History. + + The items to be processed can be explicitly set or determined by a dynamic query. + """ + return service.bulk_operation(trans, history_id, filter_query_params, payload) + + +@router.put( + "/api/histories/{history_id}/contents/{id}/validate", + summary="Validates the metadata associated with a dataset within a History.", +) +def validate( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> dict: # TODO: define a response? + """Validates the metadata associated with a dataset within a History.""" + return service.validate(trans, history_id, id) + + +@router.put( + "/api/histories/{history_id}/contents/{type}s/{id}", + summary="Updates the values for the history content item with the given ``ID`` and path specified type.", + operation_id="history_contents__update_typed", +) +def update_typed( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypePathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: UpdateHistoryContentsPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AnyHistoryContentItem: + """Updates the values for the history content item with the given ``ID``.""" + return service.update( + trans, history_id, id, payload.dict(exclude_unset=True), serialization_params, contents_type=type ) - def delete_typed( - self, - response: Response, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: str = Path(..., description="History ID or any string."), - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypePathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - purge: Optional[bool] = PurgeQueryParam, - recursive: Optional[bool] = RecursiveQueryParam, - stop_job: Optional[bool] = StopJobQueryParam, - payload: DeleteHistoryContentPayload = Body(None), - ): - """ - Delete the history content with the given ``ID`` and path specified type. - - **Note**: Currently does not stop any active jobs for which this dataset is an output. - """ - return self._delete( - response, - trans, - id, - type, - serialization_params, - purge, - recursive, - stop_job, - payload, - ) - @router.delete( - "/api/histories/{history_id}/contents/{id}", - summary="Delete the history dataset with the given ``ID``.", - responses=CONTENT_DELETE_RESPONSES, - operation_id="history_contents__delete_legacy", + +@router.put( + "/api/histories/{history_id}/contents/{id}", + summary="Updates the values for the history content item with the given ``ID`` and query specified type. ``/api/histories/{history_id}/contents/{type}s/{id}`` should be used instead.", + deprecated=True, + operation_id="history_contents__update_legacy", +) +def update_legacy( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypeQueryParam(default=HistoryContentType.dataset), + serialization_params: SerializationParams = Depends(query_serialization_params), + payload: UpdateHistoryContentsPayload = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AnyHistoryContentItem: + """Updates the values for the history content item with the given ``ID``.""" + return service.update( + trans, history_id, id, payload.dict(exclude_unset=True), serialization_params, contents_type=type ) - def delete_legacy( - self, - response: Response, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - type: HistoryContentType = ContentTypeQueryParam(default=HistoryContentType.dataset), - serialization_params: SerializationParams = Depends(query_serialization_params), - purge: Optional[bool] = PurgeQueryParam, - recursive: Optional[bool] = RecursiveQueryParam, - stop_job: Optional[bool] = StopJobQueryParam, - payload: DeleteHistoryContentPayload = Body(None), - ): - """ - Delete the history content with the given ``ID`` and query specified type (defaults to dataset). - - **Note**: Currently does not stop any active jobs for which this dataset is an output. - """ - return self._delete( - response, - trans, - id, - type, - serialization_params, - purge, - recursive, - stop_job, - payload, - ) - def _delete( - self, - response: Response, - trans: ProvidesHistoryContext, - id: DecodedDatabaseIdField, - type: HistoryContentType, - serialization_params: SerializationParams, - purge: Optional[bool], - recursive: Optional[bool], - stop_job: Optional[bool], - payload: DeleteHistoryContentPayload, - ): - # TODO: should we just use the default payload and deprecate the query params? - if payload is None: - payload = DeleteHistoryContentPayload() - payload.purge = payload.purge or purge is True - payload.recursive = payload.recursive or recursive is True - payload.stop_job = payload.stop_job or stop_job is True - rval = self.service.delete( - trans, - id=id, - serialization_params=serialization_params, - contents_type=type, - payload=payload, - ) - async_result = rval.pop("async_result", None) - if async_result: - response.status_code = status.HTTP_202_ACCEPTED - return rval - - @router.get( - "/api/histories/{history_id}/contents/archive/{filename}.{format}", - summary="Build and return a compressed archive of the selected history contents.", - operation_id="history_contents__archive_named", + +@router.delete( + "/api/histories/{history_id}/contents/{type}s/{id}", + summary="Delete the history content with the given ``ID`` and path specified type.", + responses=CONTENT_DELETE_RESPONSES, + operation_id="history_contents__delete_typed", +) +def delete_typed( + response: Response, + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: str = Path(..., description="History ID or any string."), + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypePathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + purge: Optional[bool] = PurgeQueryParam, + recursive: Optional[bool] = RecursiveQueryParam, + stop_job: Optional[bool] = StopJobQueryParam, + payload: DeleteHistoryContentPayload = Body(None), + service: HistoriesContentsService = depends(HistoriesContentsService), +): + """ + Delete the history content with the given ``ID`` and path specified type. + + **Note**: Currently does not stop any active jobs for which this dataset is an output. + """ + return _delete( + response, + trans, + id, + type, + serialization_params, + purge, + recursive, + stop_job, + payload, + service, ) - def archive_named( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - filename: str = ArchiveFilenamePathParam, - format: str = Path( - description="Output format of the archive.", - deprecated=True, # Looks like is not really used? - ), - dry_run: Optional[bool] = DryRunQueryParam, - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - ): - """Build and return a compressed archive of the selected history contents. - - **Note**: this is a volatile endpoint and settings and behavior may change.""" - archive = self.service.archive(trans, history_id, filter_query_params, filename, dry_run) - if isinstance(archive, HistoryContentsArchiveDryRunResult): - return archive - return StreamingResponse(archive.response(), headers=archive.get_headers()) - - @router.get( - "/api/histories/{history_id}/contents/archive", - summary="Build and return a compressed archive of the selected history contents.", - operation_id="history_contents__archive", + + +@router.delete( + "/api/histories/{history_id}/contents/{id}", + summary="Delete the history dataset with the given ``ID``.", + responses=CONTENT_DELETE_RESPONSES, + operation_id="history_contents__delete_legacy", +) +def delete_legacy( + response: Response, + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + type: HistoryContentType = ContentTypeQueryParam(default=HistoryContentType.dataset), + serialization_params: SerializationParams = Depends(query_serialization_params), + purge: Optional[bool] = PurgeQueryParam, + recursive: Optional[bool] = RecursiveQueryParam, + stop_job: Optional[bool] = StopJobQueryParam, + payload: DeleteHistoryContentPayload = Body(None), + service: HistoriesContentsService = depends(HistoriesContentsService), +): + """ + Delete the history content with the given ``ID`` and query specified type (defaults to dataset). + + **Note**: Currently does not stop any active jobs for which this dataset is an output. + """ + return _delete( + response, + trans, + id, + type, + serialization_params, + purge, + recursive, + stop_job, + payload, + service, ) - def archive( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - filename: Optional[str] = ArchiveFilenameQueryParam, - dry_run: Optional[bool] = DryRunQueryParam, - filter_query_params: FilterQueryParams = Depends(get_filter_query_params), - ): - """Build and return a compressed archive of the selected history contents. - - **Note**: this is a volatile endpoint and settings and behavior may change.""" - archive = self.service.archive(trans, history_id, filter_query_params, filename, dry_run) - if isinstance(archive, HistoryContentsArchiveDryRunResult): - return archive - return StreamingResponse(archive.response(), headers=archive.get_headers()) - - @router.post("/api/histories/{history_id}/contents_from_store", summary="Create contents from store.") - def create_from_store( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - serialization_params: SerializationParams = Depends(query_serialization_params), - create_payload: CreateHistoryContentFromStore = Body(...), - ) -> List[AnyHistoryContentItem]: - """ - Create history contents from model store. - Input can be a tarfile created with build_objects script distributed - with galaxy-data, from an exported history with files stripped out, - or hand-crafted JSON dictionary. - """ - return self.service.create_from_store(trans, history_id, create_payload, serialization_params) - - @router.post( - "/api/histories/{history_id}/contents/datasets/{id}/materialize", - summary="Materialize a deferred dataset into real, usable dataset.", + + +@router.get( + "/api/histories/{history_id}/contents/archive/{filename}.{format}", + summary="Build and return a compressed archive of the selected history contents.", + operation_id="history_contents__archive_named", +) +def archive_named( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + filename: str = ArchiveFilenamePathParam, + format: str = Path( + description="Output format of the archive.", + deprecated=True, # Looks like is not really used? + ), + dry_run: Optional[bool] = DryRunQueryParam, + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + service: HistoriesContentsService = depends(HistoriesContentsService), +): + """Build and return a compressed archive of the selected history contents. + + **Note**: this is a volatile endpoint and settings and behavior may change.""" + archive = service.archive(trans, history_id, filter_query_params, filename, dry_run) + if isinstance(archive, HistoryContentsArchiveDryRunResult): + return archive + return StreamingResponse(archive.response(), headers=archive.get_headers()) + + +@router.get( + "/api/histories/{history_id}/contents/archive", + summary="Build and return a compressed archive of the selected history contents.", + operation_id="history_contents__archive", +) +def archive( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + filename: Optional[str] = ArchiveFilenameQueryParam, + dry_run: Optional[bool] = DryRunQueryParam, + filter_query_params: FilterQueryParams = Depends(get_filter_query_params), + service: HistoriesContentsService = depends(HistoriesContentsService), +): + """Build and return a compressed archive of the selected history contents. + + **Note**: this is a volatile endpoint and settings and behavior may change.""" + archive = service.archive(trans, history_id, filter_query_params, filename, dry_run) + if isinstance(archive, HistoryContentsArchiveDryRunResult): + return archive + return StreamingResponse(archive.response(), headers=archive.get_headers()) + + +@router.post("/api/histories/{history_id}/contents_from_store", summary="Create contents from store.") +def create_from_store( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + serialization_params: SerializationParams = Depends(query_serialization_params), + create_payload: CreateHistoryContentFromStore = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> List[AnyHistoryContentItem]: + """ + Create history contents from model store. + Input can be a tarfile created with build_objects script distributed + with galaxy-data, from an exported history with files stripped out, + or hand-crafted JSON dictionary. + """ + return service.create_from_store(trans, history_id, create_payload, serialization_params) + + +@router.post( + "/api/histories/{history_id}/contents/datasets/{id}/materialize", + summary="Materialize a deferred dataset into real, usable dataset.", +) +def materialize_dataset( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + id: DecodedDatabaseIdField = HistoryItemIDPathParam, + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AsyncTaskResultSummary: + materialize_request = MaterializeDatasetInstanceRequest.construct( + history_id=history_id, + source=DatasetSourceType.hda, + content=id, ) - def materialize_dataset( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - id: DecodedDatabaseIdField = HistoryItemIDPathParam, - ) -> AsyncTaskResultSummary: - materialize_request = MaterializeDatasetInstanceRequest.construct( - history_id=history_id, - source=DatasetSourceType.hda, - content=id, - ) - rval = self.service.materialize(trans, materialize_request) - return rval + rval = service.materialize(trans, materialize_request) + return rval + - @router.post( - "/api/histories/{history_id}/materialize", - summary="Materialize a deferred library or HDA dataset into real, usable dataset in specified history.", +@router.post( + "/api/histories/{history_id}/materialize", + summary="Materialize a deferred library or HDA dataset into real, usable dataset in specified history.", +) +def materialize_to_history( + trans: ProvidesHistoryContext = DependsOnTrans, + history_id: DecodedDatabaseIdField = HistoryIDPathParam, + materialize_api_payload: MaterializeDatasetInstanceAPIRequest = Body(...), + service: HistoriesContentsService = depends(HistoriesContentsService), +) -> AsyncTaskResultSummary: + materialize_request: MaterializeDatasetInstanceRequest = MaterializeDatasetInstanceRequest.construct( + history_id=history_id, **materialize_api_payload.dict() ) - def materialize_to_history( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - history_id: DecodedDatabaseIdField = HistoryIDPathParam, - materialize_api_payload: MaterializeDatasetInstanceAPIRequest = Body(...), - ) -> AsyncTaskResultSummary: - materialize_request: MaterializeDatasetInstanceRequest = MaterializeDatasetInstanceRequest.construct( - history_id=history_id, **materialize_api_payload.dict() - ) - rval = self.service.materialize(trans, materialize_request) - return rval + rval = service.materialize(trans, materialize_request) + return rval + + +def _create( + trans: ProvidesHistoryContext, + history_id: DecodedDatabaseIdField, + type: Optional[HistoryContentType], + serialization_params: SerializationParams, + payload: CreateHistoryContentPayload, + service: HistoriesContentsService, +) -> Union[AnyHistoryContentItem, List[AnyHistoryContentItem]]: + """Create a new `HDA` or `HDCA` in the given History.""" + payload.type = type or payload.type + return service.create(trans, history_id, payload, serialization_params) + + +def _delete( + response: Response, + trans: ProvidesHistoryContext, + id: DecodedDatabaseIdField, + type: HistoryContentType, + serialization_params: SerializationParams, + purge: Optional[bool], + recursive: Optional[bool], + stop_job: Optional[bool], + payload: DeleteHistoryContentPayload, + service: HistoriesContentsService, +): + # TODO: should we just use the default payload and deprecate the query params? + if payload is None: + payload = DeleteHistoryContentPayload() + payload.purge = payload.purge or purge is True + payload.recursive = payload.recursive or recursive is True + payload.stop_job = payload.stop_job or stop_job is True + rval = service.delete( + trans, + id=id, + serialization_params=serialization_params, + contents_type=type, + payload=payload, + ) + async_result = rval.pop("async_result", None) + if async_result: + response.status_code = status.HTTP_202_ACCEPTED + return rval diff --git a/lib/galaxy/webapps/galaxy/api/item_tags.py b/lib/galaxy/webapps/galaxy/api/item_tags.py index a32805ac4d49..61d361b59229 100644 --- a/lib/galaxy/webapps/galaxy/api/item_tags.py +++ b/lib/galaxy/webapps/galaxy/api/item_tags.py @@ -27,10 +27,7 @@ router = Router() -@router.cbv class FastAPIItemTags: - manager: ItemTagsManager = depends(ItemTagsManager) - @classmethod def create_class(cls, prefix, tagged_item_class, tagged_item_id, api_docs_tag, extra_path_params): class Temp(cls): @@ -41,11 +38,11 @@ class Temp(cls): openapi_extra=extra_path_params, ) def index( - self, trans: ProvidesAppContext = DependsOnTrans, item_id: DecodedDatabaseIdField = Path(..., title="Item ID", alias=tagged_item_id), + manager: ItemTagsManager = depends(ItemTagsManager), ) -> ItemTagsListResponse: - return self.manager.index(trans, tagged_item_class, item_id) + return manager.index(trans, tagged_item_class, item_id) @router.get( f"/api/{prefix}/{{{tagged_item_id}}}/tags/{{tag_name}}", @@ -54,12 +51,12 @@ def index( openapi_extra=extra_path_params, ) def show( - self, trans: ProvidesAppContext = DependsOnTrans, item_id: DecodedDatabaseIdField = Path(..., title="Item ID", alias=tagged_item_id), tag_name: str = Path(..., title="Tag Name"), + manager: ItemTagsManager = depends(ItemTagsManager), ) -> ItemTagsResponse: - return self.manager.show(trans, tagged_item_class, item_id, tag_name) + return manager.show(trans, tagged_item_class, item_id, tag_name) @router.post( f"/api/{prefix}/{{{tagged_item_id}}}/tags/{{tag_name}}", @@ -68,15 +65,15 @@ def show( openapi_extra=extra_path_params, ) def create( - self, trans: ProvidesAppContext = DependsOnTrans, item_id: DecodedDatabaseIdField = Path(..., title="Item ID", alias=tagged_item_id), tag_name: str = Path(..., title="Tag Name"), payload: ItemTagsCreatePayload = Body(None), + manager: ItemTagsManager = depends(ItemTagsManager), ) -> ItemTagsResponse: if payload is None: payload = ItemTagsCreatePayload() - return self.manager.create(trans, tagged_item_class, item_id, tag_name, payload) + return manager.create(trans, tagged_item_class, item_id, tag_name, payload) @router.put( f"/api/{prefix}/{{{tagged_item_id}}}/tags/{{tag_name}}", @@ -85,13 +82,13 @@ def create( openapi_extra=extra_path_params, ) def update( - self, trans: ProvidesAppContext = DependsOnTrans, item_id: DecodedDatabaseIdField = Path(..., title="Item ID", alias=tagged_item_id), tag_name: str = Path(..., title="Tag Name"), payload: ItemTagsCreatePayload = Body(...), + manager: ItemTagsManager = depends(ItemTagsManager), ) -> ItemTagsResponse: - return self.manager.create(trans, tagged_item_class, item_id, tag_name, payload) + return manager.create(trans, tagged_item_class, item_id, tag_name, payload) @router.delete( f"/api/{prefix}/{{{tagged_item_id}}}/tags/{{tag_name}}", @@ -100,12 +97,12 @@ def update( openapi_extra=extra_path_params, ) def delete( - self, trans: ProvidesAppContext = DependsOnTrans, item_id: DecodedDatabaseIdField = Path(..., title="Item ID", alias=tagged_item_id), tag_name: str = Path(..., title="Tag Name"), + manager: ItemTagsManager = depends(ItemTagsManager), ) -> bool: - return self.manager.delete(trans, tagged_item_class, item_id, tag_name) + return manager.delete(trans, tagged_item_class, item_id, tag_name) return Temp @@ -130,7 +127,7 @@ def delete( ] } - router.cbv(FastAPIItemTags.create_class(prefix, tagged_item_class, tagged_item_id, api_docs_tag, extra_path_params)) + FastAPIItemTags.create_class(prefix, tagged_item_class, tagged_item_id, api_docs_tag, extra_path_params) # TODO: Visualization and Pages once APIs for those are available diff --git a/lib/galaxy/webapps/galaxy/api/job_tokens.py b/lib/galaxy/webapps/galaxy/api/job_tokens.py index 63907f662cd9..761fdd47c8af 100644 --- a/lib/galaxy/webapps/galaxy/api/job_tokens.py +++ b/lib/galaxy/webapps/galaxy/api/job_tokens.py @@ -22,48 +22,46 @@ router = Router(tags=["remote files"]) -@router.cbv -class FastAPIJobTokens: - @router.get( - "/api/jobs/{job_id}/oidc-tokens", - summary="Get a fresh OIDC token", - description="Allows remote job running mechanisms to get a fresh OIDC token that " - "can be used on remote side to authorize user. " - "It is not meant to represent part of Galaxy's stable, user facing API", - tags=["oidc_tokens"], - response_class=PlainTextResponse, - ) - def get_token( - self, - job_id: EncodedDatabaseIdField, - job_key: str = Query( - description=( - "A key used to authenticate this request as acting on" "behalf or a job runner for the specified job" - ), - ), - provider: str = Query( - description=("OIDC provider name"), +@router.get( + "/api/jobs/{job_id}/oidc-tokens", + summary="Get a fresh OIDC token", + description="Allows remote job running mechanisms to get a fresh OIDC token that " + "can be used on remote side to authorize user. " + "It is not meant to represent part of Galaxy's stable, user facing API", + tags=["oidc_tokens"], + response_class=PlainTextResponse, +) +def get_token( + job_id: EncodedDatabaseIdField, + job_key: str = Query( + description=( + "A key used to authenticate this request as acting on" "behalf or a job runner for the specified job" ), - trans: ProvidesAppContext = DependsOnTrans, - ) -> str: - job = self.__authorize_job_access(trans, job_id, job_key) - trans.app.authnz_manager.refresh_expiring_oidc_tokens(trans, job.user) # type: ignore[attr-defined] - tokens = job.user.get_oidc_tokens(provider_name_to_backend(provider)) - return tokens["id"] + ), + provider: str = Query( + description=("OIDC provider name"), + ), + trans: ProvidesAppContext = DependsOnTrans, +) -> str: + job = _authorize_job_access(trans, job_id, job_key) + trans.app.authnz_manager.refresh_expiring_oidc_tokens(trans, job.user) # type: ignore[attr-defined] + tokens = job.user.get_oidc_tokens(provider_name_to_backend(provider)) + return tokens["id"] + - def __authorize_job_access(self, trans, encoded_job_id, job_key): - session = trans.sa_session - job_id = trans.security.decode_id(encoded_job_id) - job = session.get(Job, job_id) - secret = job.destination_params.get("job_secret_base", "jobs_token") +def _authorize_job_access(trans, encoded_job_id, job_key): + session = trans.sa_session + job_id = trans.security.decode_id(encoded_job_id) + job = session.get(Job, job_id) + secret = job.destination_params.get("job_secret_base", "jobs_token") - job_key_internal = trans.security.encode_id(job_id, kind=secret) - if not util.safe_str_cmp(job_key_internal, job_key): - raise exceptions.AuthenticationFailed("Invalid job_key supplied.") + job_key_internal = trans.security.encode_id(job_id, kind=secret) + if not util.safe_str_cmp(job_key_internal, job_key): + raise exceptions.AuthenticationFailed("Invalid job_key supplied.") - # Verify job is active - job = session.get(Job, job_id) - if job.finished: - error_message = "Attempting to get oidc token for a job that has already completed." - raise exceptions.ItemAccessibilityException(error_message) - return job + # Verify job is active + job = session.get(Job, job_id) + if job.finished: + error_message = "Attempting to get oidc token for a job that has already completed." + raise exceptions.ItemAccessibilityException(error_message) + return job diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index bd19370dd6ae..1946342f56ae 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -197,329 +197,336 @@ DeleteJobBody = Body(title="Delete/cancel job", description="The values to delete/cancel a job") -@router.cbv -class FastAPIJobs: - service: JobsService = depends(JobsService) - - @router.get("/api/jobs") - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - states: Optional[List[str]] = Depends(query_parameter_as_list(StateQueryParam)), - user_details: bool = UserDetailsQueryParam, - user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, - view: JobIndexViewEnum = ViewQueryParam, - tool_ids: Optional[List[str]] = Depends(query_parameter_as_list(ToolIdQueryParam)), - tool_ids_like: Optional[List[str]] = Depends(query_parameter_as_list(ToolIdLikeQueryParam)), - date_range_min: Optional[Union[datetime, date]] = DateRangeMinQueryParam, - date_range_max: Optional[Union[datetime, date]] = DateRangeMaxQueryParam, - history_id: Optional[DecodedDatabaseIdField] = HistoryIdQueryParam, - workflow_id: Optional[DecodedDatabaseIdField] = WorkflowIdQueryParam, - invocation_id: Optional[DecodedDatabaseIdField] = InvocationIdQueryParam, - order_by: JobIndexSortByEnum = SortByQueryParam, - search: Optional[str] = SearchQueryParam, - limit: int = LimitQueryParam, - offset: int = OffsetQueryParam, - ) -> List[Dict[str, Any]]: - payload = JobIndexPayload.construct( - states=states, - user_details=user_details, - user_id=user_id, - view=view, - tool_ids=tool_ids, - tool_ids_like=tool_ids_like, - date_range_min=date_range_min, - date_range_max=date_range_max, - history_id=history_id, - workflow_id=workflow_id, - invocation_id=invocation_id, - order_by=order_by, - search=search, - limit=limit, - offset=offset, - ) - return self.service.index(trans, payload) - - @router.get( - "/api/jobs/{job_id}/common_problems", - name="check_common_problems", - summary="Check inputs and job for common potential problems to aid in error reporting", - ) - def common_problems( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - ) -> JobInputSummary: - job = self.service.get_job(trans=trans, job_id=job_id) - seen_ids = set() - has_empty_inputs = False - has_duplicate_inputs = False - for job_input_assoc in job.input_datasets: - input_dataset_instance = job_input_assoc.dataset - if input_dataset_instance is None: - continue - if input_dataset_instance.get_total_size() == 0: - has_empty_inputs = True - input_instance_id = input_dataset_instance.id - if input_instance_id in seen_ids: - has_duplicate_inputs = True - else: - seen_ids.add(input_instance_id) - # TODO: check percent of failing jobs around a window on job.update_time for handler - report if high. - # TODO: check percent of failing jobs around a window on job.update_time for destination_id - report if high. - # TODO: sniff inputs (add flag to allow checking files?) - return JobInputSummary(has_empty_inputs=has_empty_inputs, has_duplicate_inputs=has_duplicate_inputs) - - @router.put( - "/api/jobs/{job_id}/resume", - name="resume_paused_job", - summary="Resumes a paused job.", +@router.get("/api/jobs") +def index( + trans: ProvidesUserContext = DependsOnTrans, + states: Optional[List[str]] = Depends(query_parameter_as_list(StateQueryParam)), + user_details: bool = UserDetailsQueryParam, + user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, + view: JobIndexViewEnum = ViewQueryParam, + tool_ids: Optional[List[str]] = Depends(query_parameter_as_list(ToolIdQueryParam)), + tool_ids_like: Optional[List[str]] = Depends(query_parameter_as_list(ToolIdLikeQueryParam)), + date_range_min: Optional[Union[datetime, date]] = DateRangeMinQueryParam, + date_range_max: Optional[Union[datetime, date]] = DateRangeMaxQueryParam, + history_id: Optional[DecodedDatabaseIdField] = HistoryIdQueryParam, + workflow_id: Optional[DecodedDatabaseIdField] = WorkflowIdQueryParam, + invocation_id: Optional[DecodedDatabaseIdField] = InvocationIdQueryParam, + order_by: JobIndexSortByEnum = SortByQueryParam, + search: Optional[str] = SearchQueryParam, + limit: int = LimitQueryParam, + offset: int = OffsetQueryParam, + service: JobsService = depends(JobsService), +) -> List[Dict[str, Any]]: + payload = JobIndexPayload.construct( + states=states, + user_details=user_details, + user_id=user_id, + view=view, + tool_ids=tool_ids, + tool_ids_like=tool_ids_like, + date_range_min=date_range_min, + date_range_max=date_range_max, + history_id=history_id, + workflow_id=workflow_id, + invocation_id=invocation_id, + order_by=order_by, + search=search, + limit=limit, + offset=offset, ) - def resume( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - ) -> List[JobOutputAssociation]: - job = self.service.get_job(trans, job_id=job_id) - if not job: - raise exceptions.ObjectNotFound("Could not access job with the given id") - if job.state == job.states.PAUSED: - job.resume() + return service.index(trans, payload) + + +@router.get( + "/api/jobs/{job_id}/common_problems", + name="check_common_problems", + summary="Check inputs and job for common potential problems to aid in error reporting", +) +def common_problems( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> JobInputSummary: + job = service.get_job(trans=trans, job_id=job_id) + seen_ids = set() + has_empty_inputs = False + has_duplicate_inputs = False + for job_input_assoc in job.input_datasets: + input_dataset_instance = job_input_assoc.dataset + if input_dataset_instance is None: + continue + if input_dataset_instance.get_total_size() == 0: + has_empty_inputs = True + input_instance_id = input_dataset_instance.id + if input_instance_id in seen_ids: + has_duplicate_inputs = True else: - exceptions.RequestParameterInvalidException(f"Job with id '{job.tool_id}' is not paused") - associations = self.service.dictify_associations(trans, job.output_datasets, job.output_library_datasets) - output_associations = [] - for association in associations: - output_associations.append(JobOutputAssociation(name=association.name, dataset=association.dataset)) - return output_associations - - @router.post( - "/api/jobs/{job_id}/error", - name="report_error", - summary="Submits a bug report via the API.", - ) - def error( - self, - payload: Annotated[ReportJobErrorPayload, ReportErrorBody], - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - ) -> JobErrorSummary: - # Get dataset on which this error was triggered - dataset_id = payload.dataset_id - dataset = self.service.hda_manager.get_accessible(id=dataset_id, user=trans.user) - # Get job - job = self.service.get_job(trans, job_id) - if dataset.creating_job.id != job.id: - raise exceptions.RequestParameterInvalidException("dataset_id was not created by job_id") - tool = trans.app.toolbox.get_tool(job.tool_id, tool_version=job.tool_version) or None - email = payload.email - if not email and not trans.anonymous: - email = trans.user.email - messages = trans.app.error_reports.default_error_plugin.submit_report( - dataset=dataset, - job=job, - tool=tool, - user_submission=True, - user=trans.user, - email=email, - message=payload.message, - ) - return JobErrorSummary(messages=messages) + seen_ids.add(input_instance_id) + # TODO: check percent of failing jobs around a window on job.update_time for handler - report if high. + # TODO: check percent of failing jobs around a window on job.update_time for destination_id - report if high. + # TODO: sniff inputs (add flag to allow checking files?) + return JobInputSummary(has_empty_inputs=has_empty_inputs, has_duplicate_inputs=has_duplicate_inputs) - @router.get( - "/api/jobs/{job_id}/inputs", - name="get_inputs", - summary="Returns input datasets created by a job.", - ) - def inputs( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - ) -> List[JobInputAssociation]: - job = self.service.get_job(trans=trans, job_id=job_id) - associations = self.service.dictify_associations(trans, job.input_datasets, job.input_library_datasets) - input_associations = [] - for association in associations: - input_associations.append(JobInputAssociation(name=association.name, dataset=association.dataset)) - return input_associations - - @router.get( - "/api/jobs/{job_id}/outputs", - name="get_outputs", - summary="Returns output datasets created by a job.", - ) - def outputs( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - ) -> List[JobOutputAssociation]: - job = self.service.get_job(trans=trans, job_id=job_id) - associations = self.service.dictify_associations(trans, job.output_datasets, job.output_library_datasets) - output_associations = [] - for association in associations: - output_associations.append(JobOutputAssociation(name=association.name, dataset=association.dataset)) - return output_associations - - @router.get( - "/api/jobs/{job_id}/parameters_display", - name="resolve_parameters_display", - summary="Resolve parameters as a list for nested display.", - ) - def parameters_display_by_job( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - hda_ldda: Annotated[Optional[DatasetSourceType], DeprecatedHdaLddaQueryParam] = DatasetSourceType.hda, - trans: ProvidesUserContext = DependsOnTrans, - ) -> JobDisplayParametersSummary: - """ - Resolve parameters as a list for nested display. - This API endpoint is unstable and tied heavily to Galaxy's JS client code, - this endpoint will change frequently. - """ - hda_ldda_str = hda_ldda or "hda" - job = self.service.get_job(trans, job_id=job_id, hda_ldda=hda_ldda_str) - return summarize_job_parameters(trans, job) - - @router.get( - "/api/datasets/{dataset_id}/parameters_display", - name="resolve_parameters_display", - summary="Resolve parameters as a list for nested display.", - deprecated=True, - ) - def parameters_display_by_dataset( - self, - dataset_id: Annotated[DecodedDatabaseIdField, DatasetIdPathParam], - hda_ldda: Annotated[DatasetSourceType, HdaLddaQueryParam] = DatasetSourceType.hda, - trans: ProvidesUserContext = DependsOnTrans, - ) -> JobDisplayParametersSummary: - """ - Resolve parameters as a list for nested display. - This API endpoint is unstable and tied heavily to Galaxy's JS client code, - this endpoint will change frequently. - """ - job = self.service.get_job(trans, dataset_id=dataset_id, hda_ldda=hda_ldda) - return summarize_job_parameters(trans, job) - @router.get( - "/api/jobs/{job_id}/metrics", - name="get_metrics", - summary="Return job metrics for specified job.", - ) - def metrics_by_job( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - hda_ldda: Annotated[Optional[DatasetSourceType], DeprecatedHdaLddaQueryParam] = DatasetSourceType.hda, - trans: ProvidesUserContext = DependsOnTrans, - ) -> List[Optional[JobMetric]]: - hda_ldda_str = hda_ldda or "hda" - job = self.service.get_job(trans, job_id=job_id, hda_ldda=hda_ldda_str) - return [JobMetric(**metric) for metric in summarize_job_metrics(trans, job)] - - @router.get( - "/api/datasets/{dataset_id}/metrics", - name="get_metrics", - summary="Return job metrics for specified job.", - deprecated=True, - ) - def metrics_by_dataset( - self, - dataset_id: Annotated[DecodedDatabaseIdField, DatasetIdPathParam], - hda_ldda: Annotated[DatasetSourceType, HdaLddaQueryParam] = DatasetSourceType.hda, - trans: ProvidesUserContext = DependsOnTrans, - ) -> List[Optional[JobMetric]]: - job = self.service.get_job(trans, dataset_id=dataset_id, hda_ldda=hda_ldda) - return [JobMetric(**metric) for metric in summarize_job_metrics(trans, job)] - - @router.get( - "/api/jobs/{job_id}/destination_params", - name="destination_params_job", - summary="Return destination parameters for specified job.", - require_admin=True, - ) - def destination_params( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - ) -> JobDestinationParams: - job = self.service.get_job(trans, job_id=job_id) - return JobDestinationParams(**summarize_destination_params(trans, job)) - - @router.post( - "/api/jobs/search", - name="search_jobs", - summary="Return jobs for current user", +@router.put( + "/api/jobs/{job_id}/resume", + name="resume_paused_job", + summary="Resumes a paused job.", +) +def resume( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> List[JobOutputAssociation]: + job = service.get_job(trans, job_id=job_id) + if not job: + raise exceptions.ObjectNotFound("Could not access job with the given id") + if job.state == job.states.PAUSED: + job.resume() + else: + exceptions.RequestParameterInvalidException(f"Job with id '{job.tool_id}' is not paused") + associations = service.dictify_associations(trans, job.output_datasets, job.output_library_datasets) + output_associations = [] + for association in associations: + output_associations.append(JobOutputAssociation(name=association.name, dataset=association.dataset)) + return output_associations + + +@router.post( + "/api/jobs/{job_id}/error", + name="report_error", + summary="Submits a bug report via the API.", +) +def error( + payload: Annotated[ReportJobErrorPayload, ReportErrorBody], + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> JobErrorSummary: + # Get dataset on which this error was triggered + dataset_id = payload.dataset_id + dataset = service.hda_manager.get_accessible(id=dataset_id, user=trans.user) + # Get job + job = service.get_job(trans, job_id) + if dataset.creating_job.id != job.id: + raise exceptions.RequestParameterInvalidException("dataset_id was not created by job_id") + tool = trans.app.toolbox.get_tool(job.tool_id, tool_version=job.tool_version) or None + email = payload.email + if not email and not trans.anonymous: + email = trans.user.email + messages = trans.app.error_reports.default_error_plugin.submit_report( + dataset=dataset, + job=job, + tool=tool, + user_submission=True, + user=trans.user, + email=email, + message=payload.message, ) - def search( - self, - payload: Annotated[SearchJobsPayload, SearchJobBody], - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> List[EncodedJobDetails]: - """ - This method is designed to scan the list of previously run jobs and find records of jobs that had - the exact some input parameters and datasets. This can be used to minimize the amount of repeated work, and simply - recycle the old results. - """ - tool_id = payload.tool_id + return JobErrorSummary(messages=messages) - tool = trans.app.toolbox.get_tool(tool_id) - if tool is None: - raise exceptions.ObjectNotFound("Requested tool not found") - inputs = payload.inputs - # Find files coming in as multipart file data and add to inputs. - for k, v in payload.__annotations__.items(): - if k.startswith("files_") or k.startswith("__files_"): - inputs[k] = v - request_context = WorkRequestContext(app=trans.app, user=trans.user, history=trans.history) - all_params, all_errors, _, _ = tool.expand_incoming( - trans=trans, incoming=inputs, request_context=request_context + +@router.get( + "/api/jobs/{job_id}/inputs", + name="get_inputs", + summary="Returns input datasets created by a job.", +) +def inputs( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> List[JobInputAssociation]: + job = service.get_job(trans=trans, job_id=job_id) + associations = service.dictify_associations(trans, job.input_datasets, job.input_library_datasets) + input_associations = [] + for association in associations: + input_associations.append(JobInputAssociation(name=association.name, dataset=association.dataset)) + return input_associations + + +@router.get( + "/api/jobs/{job_id}/outputs", + name="get_outputs", + summary="Returns output datasets created by a job.", +) +def outputs( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> List[JobOutputAssociation]: + job = service.get_job(trans=trans, job_id=job_id) + associations = service.dictify_associations(trans, job.output_datasets, job.output_library_datasets) + output_associations = [] + for association in associations: + output_associations.append(JobOutputAssociation(name=association.name, dataset=association.dataset)) + return output_associations + + +@router.get( + "/api/jobs/{job_id}/parameters_display", + name="resolve_parameters_display", + summary="Resolve parameters as a list for nested display.", +) +def parameters_display_by_job( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + hda_ldda: Annotated[Optional[DatasetSourceType], DeprecatedHdaLddaQueryParam] = DatasetSourceType.hda, + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> JobDisplayParametersSummary: + """ + Resolve parameters as a list for nested display. + This API endpoint is unstable and tied heavily to Galaxy's JS client code, + this endpoint will change frequently. + """ + hda_ldda_str = hda_ldda or "hda" + job = service.get_job(trans, job_id=job_id, hda_ldda=hda_ldda_str) + return summarize_job_parameters(trans, job) + + +@router.get( + "/api/datasets/{dataset_id}/parameters_display", + name="resolve_parameters_display", + summary="Resolve parameters as a list for nested display.", + deprecated=True, +) +def parameters_display_by_dataset( + dataset_id: Annotated[DecodedDatabaseIdField, DatasetIdPathParam], + hda_ldda: Annotated[DatasetSourceType, HdaLddaQueryParam] = DatasetSourceType.hda, + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> JobDisplayParametersSummary: + """ + Resolve parameters as a list for nested display. + This API endpoint is unstable and tied heavily to Galaxy's JS client code, + this endpoint will change frequently. + """ + job = service.get_job(trans, dataset_id=dataset_id, hda_ldda=hda_ldda) + return summarize_job_parameters(trans, job) + + +@router.get( + "/api/jobs/{job_id}/metrics", + name="get_metrics", + summary="Return job metrics for specified job.", +) +def metrics_by_job( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + hda_ldda: Annotated[Optional[DatasetSourceType], DeprecatedHdaLddaQueryParam] = DatasetSourceType.hda, + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> List[Optional[JobMetric]]: + hda_ldda_str = hda_ldda or "hda" + job = service.get_job(trans, job_id=job_id, hda_ldda=hda_ldda_str) + return [JobMetric(**metric) for metric in summarize_job_metrics(trans, job)] + + +@router.get( + "/api/datasets/{dataset_id}/metrics", + name="get_metrics", + summary="Return job metrics for specified job.", + deprecated=True, +) +def metrics_by_dataset( + dataset_id: Annotated[DecodedDatabaseIdField, DatasetIdPathParam], + hda_ldda: Annotated[DatasetSourceType, HdaLddaQueryParam] = DatasetSourceType.hda, + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> List[Optional[JobMetric]]: + job = service.get_job(trans, dataset_id=dataset_id, hda_ldda=hda_ldda) + return [JobMetric(**metric) for metric in summarize_job_metrics(trans, job)] + + +@router.get( + "/api/jobs/{job_id}/destination_params", + name="destination_params_job", + summary="Return destination parameters for specified job.", + require_admin=True, +) +def destination_params( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> JobDestinationParams: + job = service.get_job(trans, job_id=job_id) + return JobDestinationParams(**summarize_destination_params(trans, job)) + + +@router.post( + "/api/jobs/search", + name="search_jobs", + summary="Return jobs for current user", +) +def search( + payload: Annotated[SearchJobsPayload, SearchJobBody], + trans: ProvidesHistoryContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> List[EncodedJobDetails]: + """ + This method is designed to scan the list of previously run jobs and find records of jobs that had + the exact some input parameters and datasets. This can be used to minimize the amount of repeated work, and simply + recycle the old results. + """ + tool_id = payload.tool_id + + tool = trans.app.toolbox.get_tool(tool_id) + if tool is None: + raise exceptions.ObjectNotFound("Requested tool not found") + inputs = payload.inputs + # Find files coming in as multipart file data and add to inputs. + for k, v in payload.__annotations__.items(): + if k.startswith("files_") or k.startswith("__files_"): + inputs[k] = v + request_context = WorkRequestContext(app=trans.app, user=trans.user, history=trans.history) + all_params, all_errors, _, _ = tool.expand_incoming(trans=trans, incoming=inputs, request_context=request_context) + if any(all_errors): + return [] + params_dump = [tool.params_to_strings(param, trans.app, nested=True) for param in all_params] + jobs = [] + for param_dump, param in zip(params_dump, all_params): + job = service.job_search.by_tool_input( + trans=trans, + tool_id=tool_id, + tool_version=tool.version, + param=param, + param_dump=param_dump, + job_state=payload.state, ) - if any(all_errors): - return [] - params_dump = [tool.params_to_strings(param, trans.app, nested=True) for param in all_params] - jobs = [] - for param_dump, param in zip(params_dump, all_params): - job = self.service.job_search.by_tool_input( - trans=trans, - tool_id=tool_id, - tool_version=tool.version, - param=param, - param_dump=param_dump, - job_state=payload.state, - ) - if job: - jobs.append(job) - return [EncodedJobDetails(**single_job.to_dict("element")) for single_job in jobs] - - @router.get( - "/api/jobs/{job_id}", - name="show_job", - summary="Return dictionary containing description of job data.", - ) - def show( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - full: Annotated[Optional[bool], FullShowQueryParam] = False, - trans: ProvidesUserContext = DependsOnTrans, - ) -> Dict[str, Any]: - return self.service.show(trans, job_id, bool(full)) - - @router.delete( - "/api/jobs/{job_id}", - name="cancel_job", - summary="Cancels specified job", - ) - def delete( - self, - job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], - trans: ProvidesUserContext = DependsOnTrans, - payload: Annotated[Optional[DeleteJobPayload], DeleteJobBody] = None, - ) -> bool: - job = self.service.get_job(trans=trans, job_id=job_id) - if payload: - message = payload.message - else: - message = None - return self.service.job_manager.stop(job, message=message) + if job: + jobs.append(job) + return [EncodedJobDetails(**single_job.to_dict("element")) for single_job in jobs] + + +@router.get( + "/api/jobs/{job_id}", + name="show_job", + summary="Return dictionary containing description of job data.", +) +def show( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + full: Annotated[Optional[bool], FullShowQueryParam] = False, + trans: ProvidesUserContext = DependsOnTrans, + service: JobsService = depends(JobsService), +) -> Dict[str, Any]: + return service.show(trans, job_id, bool(full)) + + +@router.delete( + "/api/jobs/{job_id}", + name="cancel_job", + summary="Cancels specified job", +) +def delete( + job_id: Annotated[DecodedDatabaseIdField, JobIdPathParam], + trans: ProvidesUserContext = DependsOnTrans, + payload: Annotated[Optional[DeleteJobPayload], DeleteJobBody] = None, + service: JobsService = depends(JobsService), +) -> bool: + job = service.get_job(trans=trans, job_id=job_id) + if payload: + message = payload.message + else: + message = None + return service.job_manager.stop(job, message=message) class JobController(BaseGalaxyAPIController, UsesVisualizationMixin): diff --git a/lib/galaxy/webapps/galaxy/api/libraries.py b/lib/galaxy/webapps/galaxy/api/libraries.py index 9f2a4946414c..cb373a778778 100644 --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -55,163 +55,167 @@ ) -@router.cbv -class FastAPILibraries: - service: LibrariesService = depends(LibrariesService) +@router.get( + "/api/libraries", + summary="Returns a list of summary data for all libraries.", +) +def index( + trans: ProvidesUserContext = DependsOnTrans, + deleted: Optional[bool] = DeletedQueryParam, + service: LibrariesService = depends(LibrariesService), +) -> LibrarySummaryList: + """Returns a list of summary data for all libraries.""" + return service.index(trans, deleted) + + +@router.get( + "/api/libraries/deleted", + summary="Returns a list of summary data for all libraries marked as deleted.", +) +def index_deleted( + trans: ProvidesUserContext = DependsOnTrans, + service: LibrariesService = depends(LibrariesService), +) -> LibrarySummaryList: + """Returns a list of summary data for all libraries marked as deleted.""" + return service.index(trans, True) - @router.get( - "/api/libraries", - summary="Returns a list of summary data for all libraries.", - ) - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - deleted: Optional[bool] = DeletedQueryParam, - ) -> LibrarySummaryList: - """Returns a list of summary data for all libraries.""" - return self.service.index(trans, deleted) - - @router.get( - "/api/libraries/deleted", - summary="Returns a list of summary data for all libraries marked as deleted.", - ) - def index_deleted( - self, - trans: ProvidesUserContext = DependsOnTrans, - ) -> LibrarySummaryList: - """Returns a list of summary data for all libraries marked as deleted.""" - return self.service.index(trans, True) - - @router.get( - "/api/libraries/{id}", - summary="Returns summary information about a particular library.", - ) - def show( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = LibraryIdPathParam, - ) -> LibrarySummary: - """Returns summary information about a particular library.""" - return self.service.show(trans, id) - - @router.post( - "/api/libraries", - summary="Creates a new library and returns its summary information.", - require_admin=True, - ) - def create( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: CreateLibraryPayload = Body(...), - ) -> LibrarySummary: - """Creates a new library and returns its summary information. Currently, only admin users can create libraries.""" - return self.service.create(trans, payload) - - @router.post( - "/api/libraries/from_store", - summary="Create libraries from a model store.", - require_admin=True, - ) - def create_from_store( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: CreateLibrariesFromStore = Body(...), - ) -> List[LibrarySummary]: - return self.service.create_from_store(trans, payload) - - @router.patch( - "/api/libraries/{id}", - summary="Updates the information of an existing library.", - ) - def update( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = LibraryIdPathParam, - payload: UpdateLibraryPayload = Body(...), - ) -> LibrarySummary: - """ - Updates the information of an existing library. - """ - return self.service.update(trans, id, payload) - - @router.delete( - "/api/libraries/{id}", - summary="Marks the specified library as deleted (or undeleted).", - require_admin=True, - ) - def delete( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = LibraryIdPathParam, - undelete: Optional[bool] = UndeleteQueryParam, - payload: Optional[DeleteLibraryPayload] = Body(default=None), - ) -> LibrarySummary: - """Marks the specified library as deleted (or undeleted). - Currently, only admin users can delete or restore libraries.""" - if payload: - undelete = payload.undelete - return self.service.delete(trans, id, undelete) - - @router.get( - "/api/libraries/{id}/permissions", - summary="Gets the current or available permissions of a particular library.", - ) - def get_permissions( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = LibraryIdPathParam, - scope: Optional[LibraryPermissionScope] = Query( - None, - title="Scope", - description="The scope of the permissions to retrieve. Either the `current` permissions or the `available`.", - ), - is_library_access: Optional[bool] = Query( - None, - title="Is Library Access", - description="Indicates whether the roles available for the library access are requested.", - ), - page: int = Query( - default=1, title="Page", description="The page number to retrieve when paginating the available roles." - ), - page_limit: int = Query( - default=10, title="Page Limit", description="The maximum number of permissions per page when paginating." - ), - q: Optional[str] = Query( - None, title="Query", description="Optional search text to retrieve only the roles matching this query." - ), - ) -> Union[LibraryCurrentPermissions, LibraryAvailablePermissions]: - """Gets the current or available permissions of a particular library. - The results can be paginated and additionally filtered by a query.""" - return self.service.get_permissions( - trans, - id, - scope, - is_library_access, - page, - page_limit, - q, - ) - - @router.post( - "/api/libraries/{id}/permissions", - summary="Sets the permissions to access and manipulate a library.", + +@router.get( + "/api/libraries/{id}", + summary="Returns summary information about a particular library.", +) +def show( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = LibraryIdPathParam, + service: LibrariesService = depends(LibrariesService), +) -> LibrarySummary: + """Returns summary information about a particular library.""" + return service.show(trans, id) + + +@router.post( + "/api/libraries", + summary="Creates a new library and returns its summary information.", + require_admin=True, +) +def create( + trans: ProvidesUserContext = DependsOnTrans, + payload: CreateLibraryPayload = Body(...), + service: LibrariesService = depends(LibrariesService), +) -> LibrarySummary: + """Creates a new library and returns its summary information. Currently, only admin users can create libraries.""" + return service.create(trans, payload) + + +@router.post( + "/api/libraries/from_store", + summary="Create libraries from a model store.", + require_admin=True, +) +def create_from_store( + trans: ProvidesUserContext = DependsOnTrans, + payload: CreateLibrariesFromStore = Body(...), + service: LibrariesService = depends(LibrariesService), +) -> List[LibrarySummary]: + return service.create_from_store(trans, payload) + + +@router.patch( + "/api/libraries/{id}", + summary="Updates the information of an existing library.", +) +def update( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = LibraryIdPathParam, + payload: UpdateLibraryPayload = Body(...), + service: LibrariesService = depends(LibrariesService), +) -> LibrarySummary: + """ + Updates the information of an existing library. + """ + return service.update(trans, id, payload) + + +@router.delete( + "/api/libraries/{id}", + summary="Marks the specified library as deleted (or undeleted).", + require_admin=True, +) +def delete( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = LibraryIdPathParam, + undelete: Optional[bool] = UndeleteQueryParam, + payload: Optional[DeleteLibraryPayload] = Body(default=None), + service: LibrariesService = depends(LibrariesService), +) -> LibrarySummary: + """Marks the specified library as deleted (or undeleted). + Currently, only admin users can delete or restore libraries.""" + if payload: + undelete = payload.undelete + return service.delete(trans, id, undelete) + + +@router.get( + "/api/libraries/{id}/permissions", + summary="Gets the current or available permissions of a particular library.", +) +def get_permissions( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = LibraryIdPathParam, + scope: Optional[LibraryPermissionScope] = Query( + None, + title="Scope", + description="The scope of the permissions to retrieve. Either the `current` permissions or the `available`.", + ), + is_library_access: Optional[bool] = Query( + None, + title="Is Library Access", + description="Indicates whether the roles available for the library access are requested.", + ), + page: int = Query( + default=1, title="Page", description="The page number to retrieve when paginating the available roles." + ), + page_limit: int = Query( + default=10, title="Page Limit", description="The maximum number of permissions per page when paginating." + ), + q: Optional[str] = Query( + None, title="Query", description="Optional search text to retrieve only the roles matching this query." + ), + service: LibrariesService = depends(LibrariesService), +) -> Union[LibraryCurrentPermissions, LibraryAvailablePermissions]: + """Gets the current or available permissions of a particular library. + The results can be paginated and additionally filtered by a query.""" + return service.get_permissions( + trans, + id, + scope, + is_library_access, + page, + page_limit, + q, ) - def set_permissions( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = LibraryIdPathParam, - action: Optional[LibraryPermissionAction] = Query( - default=None, - title="Action", - description="Indicates what action should be performed on the Library.", - ), - payload: Union[ - LibraryPermissionsPayload, - LegacyLibraryPermissionsPayload, - ] = Body(...), - ) -> Union[LibraryLegacySummary, LibraryCurrentPermissions]: # Old legacy response - """Sets the permissions to access and manipulate a library.""" - payload_dict = payload.dict(by_alias=True) - if isinstance(payload, LibraryPermissionsPayload) and action is not None: - payload_dict["action"] = action - return self.service.set_permissions(trans, id, payload_dict) + + +@router.post( + "/api/libraries/{id}/permissions", + summary="Sets the permissions to access and manipulate a library.", +) +def set_permissions( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = LibraryIdPathParam, + action: Optional[LibraryPermissionAction] = Query( + default=None, + title="Action", + description="Indicates what action should be performed on the Library.", + ), + payload: Union[ + LibraryPermissionsPayload, + LegacyLibraryPermissionsPayload, + ] = Body(...), + service: LibrariesService = depends(LibrariesService), +) -> Union[LibraryLegacySummary, LibraryCurrentPermissions]: # Old legacy response + """Sets the permissions to access and manipulate a library.""" + payload_dict = payload.dict(by_alias=True) + if isinstance(payload, LibraryPermissionsPayload) and action is not None: + payload_dict["action"] = action + return service.set_permissions(trans, id, payload_dict) diff --git a/lib/galaxy/webapps/galaxy/api/licenses.py b/lib/galaxy/webapps/galaxy/api/licenses.py index ebb4b59529f1..339b609c0f62 100644 --- a/lib/galaxy/webapps/galaxy/api/licenses.py +++ b/lib/galaxy/webapps/galaxy/api/licenses.py @@ -21,23 +21,18 @@ ) -@router.cbv -class FastAPILicenses: - licenses_manager: LicensesManager = depends(LicensesManager) - - @router.get( - "/api/licenses", summary="Lists all available SPDX licenses", response_description="List of SPDX licenses" - ) - async def index(self) -> List[LicenseMetadataModel]: - """Returns an index with all the available [SPDX licenses](https://spdx.org/licenses/).""" - return self.licenses_manager.get_licenses() - - @router.get( - "/api/licenses/{id}", - summary="Gets the SPDX license metadata associated with the short identifier", - response_description="SPDX license metadata", - ) - async def get(self, id=LicenseIdPath) -> LicenseMetadataModel: - """Returns the license metadata associated with the given - [SPDX license short ID](https://spdx.github.io/spdx-spec/appendix-I-SPDX-license-list/).""" - return self.licenses_manager.get_license_by_id(id) +@router.get("/api/licenses", summary="Lists all available SPDX licenses", response_description="List of SPDX licenses") +async def index(licenses_manager: LicensesManager = depends(LicensesManager)) -> List[LicenseMetadataModel]: + """Returns an index with all the available [SPDX licenses](https://spdx.org/licenses/).""" + return licenses_manager.get_licenses() + + +@router.get( + "/api/licenses/{id}", + summary="Gets the SPDX license metadata associated with the short identifier", + response_description="SPDX license metadata", +) +async def get(id=LicenseIdPath, licenses_manager: LicensesManager = depends(LicensesManager)) -> LicenseMetadataModel: + """Returns the license metadata associated with the given + [SPDX license short ID](https://spdx.github.io/spdx-spec/appendix-I-SPDX-license-list/).""" + return licenses_manager.get_license_by_id(id) diff --git a/lib/galaxy/webapps/galaxy/api/metrics.py b/lib/galaxy/webapps/galaxy/api/metrics.py index e299aa6b8dad..34f92ce07c8f 100644 --- a/lib/galaxy/webapps/galaxy/api/metrics.py +++ b/lib/galaxy/webapps/galaxy/api/metrics.py @@ -26,18 +26,14 @@ router = Router(tags=["metrics"]) -@router.cbv -class FastAPIMetrics: - manager: MetricsManager = depends(MetricsManager) - - @router.post( - "/api/metrics", - summary="Records a collection of metrics.", - ) - def create( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: CreateMetricsPayload = Body(...), - ) -> Any: - """Record any metrics sent and return some status object.""" - return self.manager.create(trans, payload) +@router.post( + "/api/metrics", + summary="Records a collection of metrics.", +) +def create( + trans: ProvidesUserContext = DependsOnTrans, + payload: CreateMetricsPayload = Body(...), + manager: MetricsManager = depends(MetricsManager), +) -> Any: + """Record any metrics sent and return some status object.""" + return manager.create(trans, payload) diff --git a/lib/galaxy/webapps/galaxy/api/notifications.py b/lib/galaxy/webapps/galaxy/api/notifications.py index 0439ddf1d672..a6e4034e10bc 100644 --- a/lib/galaxy/webapps/galaxy/api/notifications.py +++ b/lib/galaxy/webapps/galaxy/api/notifications.py @@ -45,207 +45,216 @@ router = Router(tags=["notifications"]) -@router.cbv -class FastAPINotifications: - service: NotificationService = depends(NotificationService) - - @router.get( - "/api/notifications/status", - summary="Returns the current status summary of the user's notifications since a particular date.", - ) - def get_notifications_status( - self, - trans: ProvidesUserContext = DependsOnTrans, - since: OffsetNaiveDatetime = Query(), - ) -> NotificationStatusSummary: - """Anonymous users cannot receive personal notifications, only broadcasted notifications.""" - return self.service.get_notifications_status(trans, since) - - @router.get( - "/api/notifications/preferences", - summary="Returns the current user's preferences for notifications.", - ) - def get_notification_preferences( - self, - trans: ProvidesUserContext = DependsOnTrans, - ) -> UserNotificationPreferences: - """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications.""" - return self.service.get_user_notification_preferences(trans) - - @router.put( - "/api/notifications/preferences", - summary="Updates the user's preferences for notifications.", - ) - def update_notification_preferences( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: UpdateUserNotificationPreferencesRequest = Body(), - ) -> UserNotificationPreferences: - """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. - - - Can be used to completely enable/disable notifications for a particular type (category) - or to enable/disable a particular channel on each category. - """ - return self.service.update_user_notification_preferences(trans, payload) - - @router.get( - "/api/notifications", - summary="Returns the list of notifications associated with the user.", - ) - def get_user_notifications( - self, - trans: ProvidesUserContext = DependsOnTrans, - limit: Optional[int] = 20, - offset: Optional[int] = None, - ) -> UserNotificationListResponse: - """Anonymous users cannot receive personal notifications, only broadcasted notifications. - - You can use the `limit` and `offset` parameters to paginate through the notifications. - """ - return self.service.get_user_notifications(trans, limit=limit, offset=offset) - - @router.get( - "/api/notifications/broadcast/{notification_id}", - summary="Returns the information of a specific broadcasted notification.", - ) - def get_broadcasted( - self, - trans: ProvidesUserContext = DependsOnTrans, - notification_id: DecodedDatabaseIdField = Path(), - ) -> BroadcastNotificationResponse: - """Only Admin users can access inactive notifications (scheduled or recently expired).""" - return self.service.get_broadcasted_notification(trans, notification_id) - - @router.get( - "/api/notifications/broadcast", - summary="Returns all currently active broadcasted notifications.", - ) - def get_all_broadcasted( - self, - trans: ProvidesUserContext = DependsOnTrans, - ) -> BroadcastNotificationListResponse: - """Only Admin users can access inactive notifications (scheduled or recently expired).""" - return self.service.get_all_broadcasted_notifications(trans) - - @router.get( - "/api/notifications/{notification_id}", - summary="Displays information about a notification received by the user.", - ) - def show_notification( - self, - trans: ProvidesUserContext = DependsOnTrans, - notification_id: DecodedDatabaseIdField = Path(), - ) -> UserNotificationResponse: - user = self.service.get_authenticated_user(trans) - return self.service.get_user_notification(user, notification_id) - - @router.put( - "/api/notifications/broadcast/{notification_id}", - summary="Updates the state of a broadcasted notification.", - require_admin=True, - status_code=status.HTTP_204_NO_CONTENT, - ) - def update_broadcasted_notification( - self, - trans: ProvidesUserContext = DependsOnTrans, - notification_id: DecodedDatabaseIdField = Path(), - payload: NotificationBroadcastUpdateRequest = Body(), - ): - """Only Admins can update broadcasted notifications. This is useful to reschedule, edit or expire broadcasted notifications.""" - self.service.update_broadcasted_notification(trans, notification_id, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.put( - "/api/notifications/{notification_id}", - summary="Updates the state of a notification received by the user.", - status_code=status.HTTP_204_NO_CONTENT, - ) - def update_user_notification( - self, - trans: ProvidesUserContext = DependsOnTrans, - notification_id: DecodedDatabaseIdField = Path(), - payload: UserNotificationUpdateRequest = Body(), - ): - self.service.update_user_notification(trans, notification_id, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.put( - "/api/notifications", - summary="Updates a list of notifications with the requested values in a single request.", - ) - def update_user_notifications( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: UserNotificationsBatchUpdateRequest = Body(), - ) -> NotificationsBatchUpdateResponse: - return self.service.update_user_notifications(trans, set(payload.notification_ids), payload.changes) - - @router.delete( - "/api/notifications/{notification_id}", - summary="Deletes a notification received by the user.", - status_code=status.HTTP_204_NO_CONTENT, - ) - def delete_user_notification( - self, - trans: ProvidesUserContext = DependsOnTrans, - notification_id: DecodedDatabaseIdField = Path(), - ): - """When a notification is deleted, it is not immediately removed from the database, but marked as deleted. - - - It will not be returned in the list of notifications, but admins can still access it as long as it is not expired. - - It will be eventually removed from the database by a background task after the expiration time. - - Deleted notifications will be permanently deleted when the expiration time is reached. - """ - delete_request = UserNotificationUpdateRequest(deleted=True) - self.service.update_user_notification(trans, notification_id, delete_request) - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.delete( - "/api/notifications", - summary="Deletes a list of notifications received by the user in a single request.", - ) - def delete_user_notifications( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: NotificationsBatchRequest = Body(), - ) -> NotificationsBatchUpdateResponse: - delete_request = UserNotificationUpdateRequest(deleted=True) - return self.service.update_user_notifications(trans, set(payload.notification_ids), delete_request) - - @router.post( - "/api/notifications", - summary="Sends a notification to a list of recipients (users, groups or roles).", - require_admin=True, - ) - def send_notification( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: NotificationCreateRequest = Body(), - ) -> NotificationCreatedResponse: - """Sends a notification to a list of recipients (users, groups or roles).""" - return self.service.send_notification(sender_context=trans, payload=payload) - - @router.post( - "/api/notifications/broadcast", - summary="Broadcasts a notification to every user in the system.", - require_admin=True, - ) - def broadcast_notification( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: BroadcastNotificationCreateRequest = Body(), - ) -> NotificationCreatedResponse: - """Broadcasted notifications are a special kind of notification that are always accessible to all users, including anonymous users. - They are typically used to display important information such as maintenance windows or new features. - These notifications are displayed differently from regular notifications, usually in a banner at the top or bottom of the page. - - Broadcasted notifications can include action links that are displayed as buttons. - This allows users to easily perform tasks such as filling out surveys, accepting legal agreements, or accessing new tutorials. - - Some key features of broadcasted notifications include: - - They are not associated with a specific user, so they cannot be deleted or marked as read. - - They can be scheduled to be displayed in the future or to expire after a certain time. - - By default, broadcasted notifications are published immediately and expire six months after publication. - - Only admins can create, edit, reschedule, or expire broadcasted notifications as needed. - """ - return self.service.broadcast(sender_context=trans, payload=payload) +@router.get( + "/api/notifications/status", + summary="Returns the current status summary of the user's notifications since a particular date.", +) +def get_notifications_status( + trans: ProvidesUserContext = DependsOnTrans, + since: OffsetNaiveDatetime = Query(), + service: NotificationService = depends(NotificationService), +) -> NotificationStatusSummary: + """Anonymous users cannot receive personal notifications, only broadcasted notifications.""" + return service.get_notifications_status(trans, since) + + +@router.get( + "/api/notifications/preferences", + summary="Returns the current user's preferences for notifications.", +) +def get_notification_preferences( + trans: ProvidesUserContext = DependsOnTrans, + service: NotificationService = depends(NotificationService), +) -> UserNotificationPreferences: + """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications.""" + return service.get_user_notification_preferences(trans) + + +@router.put( + "/api/notifications/preferences", + summary="Updates the user's preferences for notifications.", +) +def update_notification_preferences( + trans: ProvidesUserContext = DependsOnTrans, + payload: UpdateUserNotificationPreferencesRequest = Body(), + service: NotificationService = depends(NotificationService), +) -> UserNotificationPreferences: + """Anonymous users cannot have notification preferences. They will receive only broadcasted notifications. + + - Can be used to completely enable/disable notifications for a particular type (category) + or to enable/disable a particular channel on each category. + """ + return service.update_user_notification_preferences(trans, payload) + + +@router.get( + "/api/notifications", + summary="Returns the list of notifications associated with the user.", +) +def get_user_notifications( + trans: ProvidesUserContext = DependsOnTrans, + limit: Optional[int] = 20, + offset: Optional[int] = None, + service: NotificationService = depends(NotificationService), +) -> UserNotificationListResponse: + """Anonymous users cannot receive personal notifications, only broadcasted notifications. + + You can use the `limit` and `offset` parameters to paginate through the notifications. + """ + return service.get_user_notifications(trans, limit=limit, offset=offset) + + +@router.get( + "/api/notifications/broadcast/{notification_id}", + summary="Returns the information of a specific broadcasted notification.", +) +def get_broadcasted( + trans: ProvidesUserContext = DependsOnTrans, + notification_id: DecodedDatabaseIdField = Path(), + service: NotificationService = depends(NotificationService), +) -> BroadcastNotificationResponse: + """Only Admin users can access inactive notifications (scheduled or recently expired).""" + return service.get_broadcasted_notification(trans, notification_id) + + +@router.get( + "/api/notifications/broadcast", + summary="Returns all currently active broadcasted notifications.", +) +def get_all_broadcasted( + trans: ProvidesUserContext = DependsOnTrans, + service: NotificationService = depends(NotificationService), +) -> BroadcastNotificationListResponse: + """Only Admin users can access inactive notifications (scheduled or recently expired).""" + return service.get_all_broadcasted_notifications(trans) + + +@router.get( + "/api/notifications/{notification_id}", + summary="Displays information about a notification received by the user.", +) +def show_notification( + trans: ProvidesUserContext = DependsOnTrans, + notification_id: DecodedDatabaseIdField = Path(), + service: NotificationService = depends(NotificationService), +) -> UserNotificationResponse: + user = service.get_authenticated_user(trans) + return service.get_user_notification(user, notification_id) + + +@router.put( + "/api/notifications/broadcast/{notification_id}", + summary="Updates the state of a broadcasted notification.", + require_admin=True, + status_code=status.HTTP_204_NO_CONTENT, +) +def update_broadcasted_notification( + trans: ProvidesUserContext = DependsOnTrans, + notification_id: DecodedDatabaseIdField = Path(), + payload: NotificationBroadcastUpdateRequest = Body(), + service: NotificationService = depends(NotificationService), +): + """Only Admins can update broadcasted notifications. This is useful to reschedule, edit or expire broadcasted notifications.""" + service.update_broadcasted_notification(trans, notification_id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.put( + "/api/notifications/{notification_id}", + summary="Updates the state of a notification received by the user.", + status_code=status.HTTP_204_NO_CONTENT, +) +def update_user_notification( + trans: ProvidesUserContext = DependsOnTrans, + notification_id: DecodedDatabaseIdField = Path(), + payload: UserNotificationUpdateRequest = Body(), + service: NotificationService = depends(NotificationService), +): + service.update_user_notification(trans, notification_id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.put( + "/api/notifications", + summary="Updates a list of notifications with the requested values in a single request.", +) +def update_user_notifications( + trans: ProvidesUserContext = DependsOnTrans, + payload: UserNotificationsBatchUpdateRequest = Body(), + service: NotificationService = depends(NotificationService), +) -> NotificationsBatchUpdateResponse: + return service.update_user_notifications(trans, set(payload.notification_ids), payload.changes) + + +@router.delete( + "/api/notifications/{notification_id}", + summary="Deletes a notification received by the user.", + status_code=status.HTTP_204_NO_CONTENT, +) +def delete_user_notification( + trans: ProvidesUserContext = DependsOnTrans, + notification_id: DecodedDatabaseIdField = Path(), + service: NotificationService = depends(NotificationService), +): + """When a notification is deleted, it is not immediately removed from the database, but marked as deleted. + + - It will not be returned in the list of notifications, but admins can still access it as long as it is not expired. + - It will be eventually removed from the database by a background task after the expiration time. + - Deleted notifications will be permanently deleted when the expiration time is reached. + """ + delete_request = UserNotificationUpdateRequest(deleted=True) + service.update_user_notification(trans, notification_id, delete_request) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.delete( + "/api/notifications", + summary="Deletes a list of notifications received by the user in a single request.", +) +def delete_user_notifications( + trans: ProvidesUserContext = DependsOnTrans, + payload: NotificationsBatchRequest = Body(), + service: NotificationService = depends(NotificationService), +) -> NotificationsBatchUpdateResponse: + delete_request = UserNotificationUpdateRequest(deleted=True) + return service.update_user_notifications(trans, set(payload.notification_ids), delete_request) + + +@router.post( + "/api/notifications", + summary="Sends a notification to a list of recipients (users, groups or roles).", + require_admin=True, +) +def send_notification( + trans: ProvidesUserContext = DependsOnTrans, + payload: NotificationCreateRequest = Body(), + service: NotificationService = depends(NotificationService), +) -> NotificationCreatedResponse: + """Sends a notification to a list of recipients (users, groups or roles).""" + return service.send_notification(sender_context=trans, payload=payload) + + +@router.post( + "/api/notifications/broadcast", + summary="Broadcasts a notification to every user in the system.", + require_admin=True, +) +def broadcast_notification( + trans: ProvidesUserContext = DependsOnTrans, + payload: BroadcastNotificationCreateRequest = Body(), + service: NotificationService = depends(NotificationService), +) -> NotificationCreatedResponse: + """Broadcasted notifications are a special kind of notification that are always accessible to all users, including anonymous users. + They are typically used to display important information such as maintenance windows or new features. + These notifications are displayed differently from regular notifications, usually in a banner at the top or bottom of the page. + + Broadcasted notifications can include action links that are displayed as buttons. + This allows users to easily perform tasks such as filling out surveys, accepting legal agreements, or accessing new tutorials. + + Some key features of broadcasted notifications include: + - They are not associated with a specific user, so they cannot be deleted or marked as read. + - They can be scheduled to be displayed in the future or to expire after a certain time. + - By default, broadcasted notifications are published immediately and expire six months after publication. + - Only admins can create, edit, reschedule, or expire broadcasted notifications as needed. + """ + return service.broadcast(sender_context=trans, payload=payload) diff --git a/lib/galaxy/webapps/galaxy/api/object_store.py b/lib/galaxy/webapps/galaxy/api/object_store.py index f8d7319b885f..9bbca59dc982 100644 --- a/lib/galaxy/webapps/galaxy/api/object_store.py +++ b/lib/galaxy/webapps/galaxy/api/object_store.py @@ -39,40 +39,38 @@ ) -@router.cbv -class FastAPIObjectStore: - object_store: BaseObjectStore = depends(BaseObjectStore) +@router.get( + "/api/object_stores", + summary="Get a list of (currently only concrete) object stores configured with this Galaxy instance.", + response_description="A list of the configured object stores.", +) +def index( + trans: ProvidesUserContext = DependsOnTrans, + selectable: bool = SelectableQueryParam, + object_store: BaseObjectStore = depends(BaseObjectStore), +) -> List[ConcreteObjectStoreModel]: + if not selectable: + raise RequestParameterInvalidException( + "The object store index query currently needs to be called with selectable=true" + ) + selectable_ids = object_store.object_store_ids_allowing_selection() + return [_model_for(selectable_id, object_store) for selectable_id in selectable_ids] + - @router.get( - "/api/object_stores", - summary="Get a list of (currently only concrete) object stores configured with this Galaxy instance.", - response_description="A list of the configured object stores.", - ) - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - selectable: bool = SelectableQueryParam, - ) -> List[ConcreteObjectStoreModel]: - if not selectable: - raise RequestParameterInvalidException( - "The object store index query currently needs to be called with selectable=true" - ) - selectable_ids = self.object_store.object_store_ids_allowing_selection() - return [self._model_for(selectable_id) for selectable_id in selectable_ids] +@router.get( + "/api/object_stores/{object_store_id}", + summary="Get information about a concrete object store configured with Galaxy.", +) +def show_info( + trans: ProvidesUserContext = DependsOnTrans, + object_store_id: str = ConcreteObjectStoreIdPathParam, + object_store: BaseObjectStore = depends(BaseObjectStore), +) -> ConcreteObjectStoreModel: + return _model_for(object_store_id, object_store) - @router.get( - "/api/object_stores/{object_store_id}", - summary="Get information about a concrete object store configured with Galaxy.", - ) - def show_info( - self, - trans: ProvidesUserContext = DependsOnTrans, - object_store_id: str = ConcreteObjectStoreIdPathParam, - ) -> ConcreteObjectStoreModel: - return self._model_for(object_store_id) - def _model_for(self, object_store_id: str) -> ConcreteObjectStoreModel: - concrete_object_store = self.object_store.get_concrete_store_by_object_store_id(object_store_id) - if concrete_object_store is None: - raise ObjectNotFound() - return concrete_object_store.to_model(object_store_id) +def _model_for(object_store_id: str, object_store: BaseObjectStore) -> ConcreteObjectStoreModel: + concrete_object_store = object_store.get_concrete_store_by_object_store_id(object_store_id) + if concrete_object_store is None: + raise ObjectNotFound() + return concrete_object_store.to_model(object_store_id) diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 461ca5f7f9e4..327a4d884022 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -94,214 +94,222 @@ ) -@router.cbv -class FastAPIPages: - service: PagesService = depends(PagesService) - - @router.get( - "/api/pages", - summary="Lists all Pages viewable by the user.", - response_description="A list with summary page information.", - ) - async def index( - self, - response: Response, - trans: ProvidesUserContext = DependsOnTrans, - deleted: bool = DeletedQueryParam, - user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, - show_published: bool = ShowPublishedQueryParam, - show_shared: bool = ShowSharedQueryParam, - sort_by: PageSortByEnum = SortByQueryParam, - sort_desc: bool = SortDescQueryParam, - limit: int = LimitQueryParam, - offset: int = OffsetQueryParam, - search: Optional[str] = SearchQueryParam, - ) -> PageSummaryList: - """Get a list with summary information of all Pages available to the user.""" - payload = PageIndexQueryPayload.construct( - deleted=deleted, - user_id=user_id, - show_published=show_published, - show_shared=show_shared, - sort_by=sort_by, - sort_desc=sort_desc, - limit=limit, - offset=offset, - search=search, - ) - pages, total_matches = self.service.index(trans, payload, include_total_count=True) - response.headers["total_matches"] = str(total_matches) - return pages - - @router.post( - "/api/pages", - summary="Create a page and return summary information.", - response_description="The page summary information.", - ) - def create( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: CreatePagePayload = Body(...), - ) -> PageSummary: - """Get a list with details of all Pages available to the user.""" - return self.service.create(trans, payload) - - @router.delete( - "/api/pages/{id}", - summary="Marks the specific Page as deleted.", - status_code=status.HTTP_204_NO_CONTENT, +@router.get( + "/api/pages", + summary="Lists all Pages viewable by the user.", + response_description="A list with summary page information.", +) +async def index( + response: Response, + trans: ProvidesUserContext = DependsOnTrans, + deleted: bool = DeletedQueryParam, + user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, + show_published: bool = ShowPublishedQueryParam, + show_shared: bool = ShowSharedQueryParam, + sort_by: PageSortByEnum = SortByQueryParam, + sort_desc: bool = SortDescQueryParam, + limit: int = LimitQueryParam, + offset: int = OffsetQueryParam, + search: Optional[str] = SearchQueryParam, + service: PagesService = depends(PagesService), +) -> PageSummaryList: + """Get a list with summary information of all Pages available to the user.""" + payload = PageIndexQueryPayload.construct( + deleted=deleted, + user_id=user_id, + show_published=show_published, + show_shared=show_shared, + sort_by=sort_by, + sort_desc=sort_desc, + limit=limit, + offset=offset, + search=search, ) - async def delete( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ): - """Marks the Page with the given ID as deleted.""" - self.service.delete(trans, id) - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.get( - "/api/pages/{id}.pdf", - summary="Return a PDF document of the last revision of the Page.", - response_class=StreamingResponse, - responses={ - 200: { - "description": "PDF document with the last revision of the page.", - "content": {"application/pdf": {}}, - }, - 501: {"description": "PDF conversion service not available."}, + pages, total_matches = service.index(trans, payload, include_total_count=True) + response.headers["total_matches"] = str(total_matches) + return pages + + +@router.post( + "/api/pages", + summary="Create a page and return summary information.", + response_description="The page summary information.", +) +def create( + trans: ProvidesUserContext = DependsOnTrans, + payload: CreatePagePayload = Body(...), + service: PagesService = depends(PagesService), +) -> PageSummary: + """Get a list with details of all Pages available to the user.""" + return service.create(trans, payload) + + +@router.delete( + "/api/pages/{id}", + summary="Marks the specific Page as deleted.", + status_code=status.HTTP_204_NO_CONTENT, +) +async def delete( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +): + """Marks the Page with the given ID as deleted.""" + service.delete(trans, id) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/api/pages/{id}.pdf", + summary="Return a PDF document of the last revision of the Page.", + response_class=StreamingResponse, + responses={ + 200: { + "description": "PDF document with the last revision of the page.", + "content": {"application/pdf": {}}, }, - ) - async def show_pdf( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ): - """Return a PDF document of the last revision of the Page. - - This feature may not be available in this Galaxy. - """ - pdf_bytes = self.service.show_pdf(trans, id) - return StreamingResponse(io.BytesIO(pdf_bytes), media_type="application/pdf") - - @router.post( - "/api/pages/{id}/prepare_download", - summary="Return a PDF document of the last revision of the Page.", - responses={ - 200: { - "description": "Short term storage reference for async monitoring of this download.", - }, - 501: {"description": "PDF conversion service not available."}, + 501: {"description": "PDF conversion service not available."}, + }, +) +async def show_pdf( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +): + """Return a PDF document of the last revision of the Page. + + This feature may not be available in this Galaxy. + """ + pdf_bytes = service.show_pdf(trans, id) + return StreamingResponse(io.BytesIO(pdf_bytes), media_type="application/pdf") + + +@router.post( + "/api/pages/{id}/prepare_download", + summary="Return a PDF document of the last revision of the Page.", + responses={ + 200: { + "description": "Short term storage reference for async monitoring of this download.", }, - ) - async def prepare_pdf( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> AsyncFile: - """Return a STS download link for this page to be downloaded as a PDF. - - This feature may not be available in this Galaxy. - """ - return self.service.prepare_pdf(trans, id) - - @router.get( - "/api/pages/{id}", - summary="Return a page summary and the content of the last revision.", - response_description="The page summary information.", - ) - async def show( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> PageDetails: - """Return summary information about a specific Page and the content of the last revision.""" - return self.service.show(trans, id) - - @router.get( - "/api/pages/{id}/sharing", - summary="Get the current sharing status of the given Page.", - ) - def sharing( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> SharingStatus: - """Return the sharing status of the item.""" - return self.service.shareable_service.sharing(trans, id) - - @router.put( - "/api/pages/{id}/enable_link_access", - summary="Makes this item accessible by a URL link.", - ) - def enable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> SharingStatus: - """Makes this item accessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.enable_link_access(trans, id) - - @router.put( - "/api/pages/{id}/disable_link_access", - summary="Makes this item inaccessible by a URL link.", - ) - def disable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> SharingStatus: - """Makes this item inaccessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.disable_link_access(trans, id) - - @router.put( - "/api/pages/{id}/publish", - summary="Makes this item public and accessible by a URL link.", - ) - def publish( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> SharingStatus: - """Makes this item publicly available by a URL link and return the current sharing status.""" - return self.service.shareable_service.publish(trans, id) - - @router.put( - "/api/pages/{id}/unpublish", - summary="Removes this item from the published list.", - ) - def unpublish( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - ) -> SharingStatus: - """Removes this item from the published list and return the current sharing status.""" - return self.service.shareable_service.unpublish(trans, id) - - @router.put( - "/api/pages/{id}/share_with_users", - summary="Share this item with specific users.", - ) - def share_with_users( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - payload: ShareWithPayload = Body(...), - ) -> ShareWithStatus: - """Shares this item with specific users and return the current sharing status.""" - return self.service.shareable_service.share_with_users(trans, id, payload) - - @router.put( - "/api/pages/{id}/slug", - summary="Set a new slug for this shared item.", - status_code=status.HTTP_204_NO_CONTENT, - ) - def set_slug( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = PageIdPathParam, - payload: SetSlugPayload = Body(...), - ): - """Sets a new slug to access this item by URL. The new slug must be unique.""" - self.service.shareable_service.set_slug(trans, id, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) + 501: {"description": "PDF conversion service not available."}, + }, +) +async def prepare_pdf( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> AsyncFile: + """Return a STS download link for this page to be downloaded as a PDF. + + This feature may not be available in this Galaxy. + """ + return service.prepare_pdf(trans, id) + + +@router.get( + "/api/pages/{id}", + summary="Return a page summary and the content of the last revision.", + response_description="The page summary information.", +) +async def show( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> PageDetails: + """Return summary information about a specific Page and the content of the last revision.""" + return service.show(trans, id) + + +@router.get( + "/api/pages/{id}/sharing", + summary="Get the current sharing status of the given Page.", +) +def sharing( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> SharingStatus: + """Return the sharing status of the item.""" + return service.shareable_service.sharing(trans, id) + + +@router.put( + "/api/pages/{id}/enable_link_access", + summary="Makes this item accessible by a URL link.", +) +def enable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> SharingStatus: + """Makes this item accessible by a URL link and return the current sharing status.""" + return service.shareable_service.enable_link_access(trans, id) + + +@router.put( + "/api/pages/{id}/disable_link_access", + summary="Makes this item inaccessible by a URL link.", +) +def disable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> SharingStatus: + """Makes this item inaccessible by a URL link and return the current sharing status.""" + return service.shareable_service.disable_link_access(trans, id) + + +@router.put( + "/api/pages/{id}/publish", + summary="Makes this item public and accessible by a URL link.", +) +def publish( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> SharingStatus: + """Makes this item publicly available by a URL link and return the current sharing status.""" + return service.shareable_service.publish(trans, id) + + +@router.put( + "/api/pages/{id}/unpublish", + summary="Removes this item from the published list.", +) +def unpublish( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + service: PagesService = depends(PagesService), +) -> SharingStatus: + """Removes this item from the published list and return the current sharing status.""" + return service.shareable_service.unpublish(trans, id) + + +@router.put( + "/api/pages/{id}/share_with_users", + summary="Share this item with specific users.", +) +def share_with_users( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + payload: ShareWithPayload = Body(...), + service: PagesService = depends(PagesService), +) -> ShareWithStatus: + """Shares this item with specific users and return the current sharing status.""" + return service.shareable_service.share_with_users(trans, id, payload) + + +@router.put( + "/api/pages/{id}/slug", + summary="Set a new slug for this shared item.", + status_code=status.HTTP_204_NO_CONTENT, +) +def set_slug( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = PageIdPathParam, + payload: SetSlugPayload = Body(...), + service: PagesService = depends(PagesService), +): + """Sets a new slug to access this item by URL. The new slug must be unique.""" + service.shareable_service.set_slug(trans, id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/lib/galaxy/webapps/galaxy/api/quotas.py b/lib/galaxy/webapps/galaxy/api/quotas.py index 279b66457eb4..a6dcc2bab5ee 100644 --- a/lib/galaxy/webapps/galaxy/api/quotas.py +++ b/lib/galaxy/webapps/galaxy/api/quotas.py @@ -34,118 +34,128 @@ ) -@router.cbv -class FastAPIQuota: - service: QuotasService = depends(QuotasService) - - @router.get( - "/api/quotas", - summary="Displays a list with information of quotas that are currently active.", - require_admin=True, - ) - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - ) -> QuotaSummaryList: - """Displays a list with information of quotas that are currently active.""" - return self.service.index(trans) - - @router.get( - "/api/quotas/deleted", - summary="Displays a list with information of quotas that have been deleted.", - require_admin=True, - ) - def index_deleted( - self, - trans: ProvidesUserContext = DependsOnTrans, - ) -> QuotaSummaryList: - """Displays a list with information of quotas that have been deleted.""" - return self.service.index(trans, deleted=True) - - @router.get( - "/api/quotas/{id}", - name="quota", - summary="Displays details on a particular active quota.", - require_admin=True, - ) - def show( - self, trans: ProvidesUserContext = DependsOnTrans, id: DecodedDatabaseIdField = QuotaIdPathParam - ) -> QuotaDetails: - """Displays details on a particular active quota.""" - return self.service.show(trans, id) - - @router.get( - "/api/quotas/deleted/{id}", - name="deleted_quota", - summary="Displays details on a particular quota that has been deleted.", - require_admin=True, - ) - def show_deleted( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = QuotaIdPathParam, - ) -> QuotaDetails: - """Displays details on a particular quota that has been deleted.""" - return self.service.show(trans, id, deleted=True) - - @router.post( - "/api/quotas", - summary="Creates a new quota.", - require_admin=True, - ) - def create( - self, - payload: CreateQuotaParams, - trans: ProvidesUserContext = DependsOnTrans, - ) -> CreateQuotaResult: - """Creates a new quota.""" - return self.service.create(trans, payload) - - @router.put( - "/api/quotas/{id}", - summary="Updates an existing quota.", - require_admin=True, - ) - def update( - self, - payload: UpdateQuotaParams, - id: DecodedDatabaseIdField = QuotaIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, - ) -> str: - """Updates an existing quota.""" - return self.service.update(trans, id, payload) - - @router.delete( - "/api/quotas/{id}", - summary="Deletes an existing quota.", - require_admin=True, - ) - def delete( - self, - id: DecodedDatabaseIdField = QuotaIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, - payload: DeleteQuotaPayload = Body(None), # Optional - ) -> str: - """Deletes an existing quota.""" - return self.service.delete(trans, id, payload) - - @router.post( - "/api/quotas/{id}/purge", - summary="Purges a previously deleted quota.", - require_admin=True, - ) - def purge(self, id: DecodedDatabaseIdField = QuotaIdPathParam, trans: ProvidesUserContext = DependsOnTrans) -> str: - return self.service.purge(trans, id) - - @router.post( - "/api/quotas/deleted/{id}/undelete", - summary="Restores a previously deleted quota.", - require_admin=True, - ) - def undelete( - self, - id: DecodedDatabaseIdField = QuotaIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, - ) -> str: - """Restores a previously deleted quota.""" - return self.service.undelete(trans, id) +@router.get( + "/api/quotas", + summary="Displays a list with information of quotas that are currently active.", + require_admin=True, +) +def index( + trans: ProvidesUserContext = DependsOnTrans, + service: QuotasService = depends(QuotasService), +) -> QuotaSummaryList: + """Displays a list with information of quotas that are currently active.""" + return service.index(trans) + + +@router.get( + "/api/quotas/deleted", + summary="Displays a list with information of quotas that have been deleted.", + require_admin=True, +) +def index_deleted( + trans: ProvidesUserContext = DependsOnTrans, + service: QuotasService = depends(QuotasService), +) -> QuotaSummaryList: + """Displays a list with information of quotas that have been deleted.""" + return service.index(trans, deleted=True) + + +@router.get( + "/api/quotas/{id}", + name="quota", + summary="Displays details on a particular active quota.", + require_admin=True, +) +def show( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = QuotaIdPathParam, + service: QuotasService = depends(QuotasService), +) -> QuotaDetails: + """Displays details on a particular active quota.""" + return service.show(trans, id) + + +@router.get( + "/api/quotas/deleted/{id}", + name="deleted_quota", + summary="Displays details on a particular quota that has been deleted.", + require_admin=True, +) +def show_deleted( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = QuotaIdPathParam, + service: QuotasService = depends(QuotasService), +) -> QuotaDetails: + """Displays details on a particular quota that has been deleted.""" + return service.show(trans, id, deleted=True) + + +@router.post( + "/api/quotas", + summary="Creates a new quota.", + require_admin=True, +) +def create( + payload: CreateQuotaParams, + trans: ProvidesUserContext = DependsOnTrans, + service: QuotasService = depends(QuotasService), +) -> CreateQuotaResult: + """Creates a new quota.""" + return service.create(trans, payload) + + +@router.put( + "/api/quotas/{id}", + summary="Updates an existing quota.", + require_admin=True, +) +def update( + payload: UpdateQuotaParams, + id: DecodedDatabaseIdField = QuotaIdPathParam, + trans: ProvidesUserContext = DependsOnTrans, + service: QuotasService = depends(QuotasService), +) -> str: + """Updates an existing quota.""" + return service.update(trans, id, payload) + + +@router.delete( + "/api/quotas/{id}", + summary="Deletes an existing quota.", + require_admin=True, +) +def delete( + id: DecodedDatabaseIdField = QuotaIdPathParam, + trans: ProvidesUserContext = DependsOnTrans, + payload: DeleteQuotaPayload = Body(None), # Optional + service: QuotasService = depends(QuotasService), +) -> str: + """Deletes an existing quota.""" + return service.delete(trans, id, payload) + + +@router.post( + "/api/quotas/{id}/purge", + summary="Purges a previously deleted quota.", + require_admin=True, +) +def purge( + id: DecodedDatabaseIdField = QuotaIdPathParam, + trans: ProvidesUserContext = DependsOnTrans, + service: QuotasService = depends(QuotasService), +) -> str: + return service.purge(trans, id) + + +@router.post( + "/api/quotas/deleted/{id}/undelete", + summary="Restores a previously deleted quota.", + require_admin=True, +) +def undelete( + id: DecodedDatabaseIdField = QuotaIdPathParam, + trans: ProvidesUserContext = DependsOnTrans, + service: QuotasService = depends(QuotasService), +) -> str: + """Restores a previously deleted quota.""" + return service.undelete(trans, id) diff --git a/lib/galaxy/webapps/galaxy/api/remote_files.py b/lib/galaxy/webapps/galaxy/api/remote_files.py index 5c858f39879c..0a59146cdf09 100644 --- a/lib/galaxy/webapps/galaxy/api/remote_files.py +++ b/lib/galaxy/webapps/galaxy/api/remote_files.py @@ -102,64 +102,62 @@ ) -@router.cbv -class FastAPIRemoteFiles: - manager: RemoteFilesManager = depends(RemoteFilesManager) - - @router.get( - "/api/remote_files", - summary="Displays remote files available to the user.", - response_description="A list with details about the remote files available to the user.", - ) - @router.get( - "/api/ftp_files", - deprecated=True, - summary="Displays remote files available to the user. Please use /api/remote_files instead.", - ) - async def index( - self, - user_ctx: ProvidesUserContext = DependsOnTrans, - target: str = TargetQueryParam, - format: Optional[RemoteFilesFormat] = FormatQueryParam, - recursive: Optional[bool] = RecursiveQueryParam, - disable: Optional[RemoteFilesDisableMode] = DisableModeQueryParam, - writeable: Optional[bool] = WriteableQueryParam, - ) -> AnyRemoteFilesListResponse: - """Lists all remote files available to the user from different sources.""" - return self.manager.index(user_ctx, target, format, recursive, disable, writeable) - - @router.get( - "/api/remote_files/plugins", - summary="Display plugin information for each of the gxfiles:// URI targets available.", - response_description="A list with details about each plugin.", - ) - async def plugins( - self, - user_ctx: ProvidesUserContext = DependsOnTrans, - browsable_only: Optional[bool] = BrowsableQueryParam, - include_kind: Annotated[Optional[List[PluginKind]], IncludeKindQueryParam] = None, - exclude_kind: Annotated[Optional[List[PluginKind]], ExcludeKindQueryParam] = None, - ) -> FilesSourcePluginList: - """Display plugin information for each of the gxfiles:// URI targets available.""" - return self.manager.get_files_source_plugins( - user_ctx, - browsable_only, - set(include_kind) if include_kind else None, - set(exclude_kind) if exclude_kind else None, - ) - - @router.post( - "/api/remote_files", - summary="Creates a new entry (directory/record) on the remote files source.", +@router.get( + "/api/remote_files", + summary="Displays remote files available to the user.", + response_description="A list with details about the remote files available to the user.", +) +@router.get( + "/api/ftp_files", + deprecated=True, + summary="Displays remote files available to the user. Please use /api/remote_files instead.", +) +async def index( + user_ctx: ProvidesUserContext = DependsOnTrans, + target: str = TargetQueryParam, + format: Optional[RemoteFilesFormat] = FormatQueryParam, + recursive: Optional[bool] = RecursiveQueryParam, + disable: Optional[RemoteFilesDisableMode] = DisableModeQueryParam, + writeable: Optional[bool] = WriteableQueryParam, + manager: RemoteFilesManager = depends(RemoteFilesManager), +) -> AnyRemoteFilesListResponse: + """Lists all remote files available to the user from different sources.""" + return manager.index(user_ctx, target, format, recursive, disable, writeable) + + +@router.get( + "/api/remote_files/plugins", + summary="Display plugin information for each of the gxfiles:// URI targets available.", + response_description="A list with details about each plugin.", +) +async def plugins( + user_ctx: ProvidesUserContext = DependsOnTrans, + browsable_only: Optional[bool] = BrowsableQueryParam, + include_kind: Annotated[Optional[List[PluginKind]], IncludeKindQueryParam] = None, + exclude_kind: Annotated[Optional[List[PluginKind]], ExcludeKindQueryParam] = None, + manager: RemoteFilesManager = depends(RemoteFilesManager), +) -> FilesSourcePluginList: + """Display plugin information for each of the gxfiles:// URI targets available.""" + return manager.get_files_source_plugins( + user_ctx, + browsable_only, + set(include_kind) if include_kind else None, + set(exclude_kind) if exclude_kind else None, ) - async def create_entry( - self, - user_ctx: ProvidesUserContext = DependsOnTrans, - payload: CreateEntryPayload = Body( - ..., - title="Entry Data", - description="Information about the entry to create. Depends on the target file source.", - ), - ) -> CreatedEntryResponse: - """Creates a new entry on the remote files source.""" - return self.manager.create_entry(user_ctx, payload) + + +@router.post( + "/api/remote_files", + summary="Creates a new entry (directory/record) on the remote files source.", +) +async def create_entry( + user_ctx: ProvidesUserContext = DependsOnTrans, + payload: CreateEntryPayload = Body( + ..., + title="Entry Data", + description="Information about the entry to create. Depends on the target file source.", + ), + manager: RemoteFilesManager = depends(RemoteFilesManager), +) -> CreatedEntryResponse: + """Creates a new entry on the remote files source.""" + return manager.create_entry(user_ctx, payload) diff --git a/lib/galaxy/webapps/galaxy/api/roles.py b/lib/galaxy/webapps/galaxy/api/roles.py index 53ef695a0aca..2c575562a832 100644 --- a/lib/galaxy/webapps/galaxy/api/roles.py +++ b/lib/galaxy/webapps/galaxy/api/roles.py @@ -35,41 +35,62 @@ def role_to_model(role): return RoleModelResponse(**item) -@router.cbv -class FastAPIRoles: - role_manager: RoleManager = depends(RoleManager) - - @router.get("/api/roles") - def index(self, trans: ProvidesUserContext = DependsOnTrans) -> RoleListResponse: - roles = self.role_manager.list_displayable_roles(trans) - return RoleListResponse(__root__=[role_to_model(r) for r in roles]) - - @router.get("/api/roles/{id}") - def show(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse: - role = self.role_manager.get(trans, id) - return role_to_model(role) - - @router.post("/api/roles", require_admin=True) - def create( - self, trans: ProvidesUserContext = DependsOnTrans, role_definition_model: RoleDefinitionModel = Body(...) - ) -> RoleModelResponse: - role = self.role_manager.create_role(trans, role_definition_model) - return role_to_model(role) - - @router.delete("/api/roles/{id}", require_admin=True) - def delete(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse: - role = self.role_manager.get(trans, id) - role = self.role_manager.delete(trans, role) - return role_to_model(role) - - @router.post("/api/roles/{id}/purge", require_admin=True) - def purge(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse: - role = self.role_manager.get(trans, id) - role = self.role_manager.purge(trans, role) - return role_to_model(role) - - @router.post("/api/roles/{id}/undelete", require_admin=True) - def undelete(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse: - role = self.role_manager.get(trans, id) - role = self.role_manager.undelete(trans, role) - return role_to_model(role) +@router.get("/api/roles") +def index( + trans: ProvidesUserContext = DependsOnTrans, role_manager: RoleManager = depends(RoleManager) +) -> RoleListResponse: + roles = role_manager.list_displayable_roles(trans) + return RoleListResponse(__root__=[role_to_model(r) for r in roles]) + + +@router.get("/api/roles/{id}") +def show( + id: DecodedDatabaseIdField, + trans: ProvidesUserContext = DependsOnTrans, + role_manager: RoleManager = depends(RoleManager), +) -> RoleModelResponse: + role = role_manager.get(trans, id) + return role_to_model(role) + + +@router.post("/api/roles", require_admin=True) +def create( + trans: ProvidesUserContext = DependsOnTrans, + role_definition_model: RoleDefinitionModel = Body(...), + role_manager: RoleManager = depends(RoleManager), +) -> RoleModelResponse: + role = role_manager.create_role(trans, role_definition_model) + return role_to_model(role) + + +@router.delete("/api/roles/{id}", require_admin=True) +def delete( + id: DecodedDatabaseIdField, + trans: ProvidesUserContext = DependsOnTrans, + role_manager: RoleManager = depends(RoleManager), +) -> RoleModelResponse: + role = role_manager.get(trans, id) + role = role_manager.delete(trans, role) + return role_to_model(role) + + +@router.post("/api/roles/{id}/purge", require_admin=True) +def purge( + id: DecodedDatabaseIdField, + trans: ProvidesUserContext = DependsOnTrans, + role_manager: RoleManager = depends(RoleManager), +) -> RoleModelResponse: + role = role_manager.get(trans, id) + role = role_manager.purge(trans, role) + return role_to_model(role) + + +@router.post("/api/roles/{id}/undelete", require_admin=True) +def undelete( + id: DecodedDatabaseIdField, + trans: ProvidesUserContext = DependsOnTrans, + role_manager: RoleManager = depends(RoleManager), +) -> RoleModelResponse: + role = role_manager.get(trans, id) + role = role_manager.undelete(trans, role) + return role_to_model(role) diff --git a/lib/galaxy/webapps/galaxy/api/short_term_storage.py b/lib/galaxy/webapps/galaxy/api/short_term_storage.py index cd5a95a1965f..9a74f629204a 100644 --- a/lib/galaxy/webapps/galaxy/api/short_term_storage.py +++ b/lib/galaxy/webapps/galaxy/api/short_term_storage.py @@ -17,42 +17,45 @@ router = Router(tags=["short_term_storage"]) -@router.cbv -class FastAPIShortTermStorage: - short_term_storage_monitor: ShortTermStorageMonitor = depends(ShortTermStorageMonitor) # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 +@router.get( + "/api/short_term_storage/{storage_request_id}/ready", + summary="Determine if specified storage request ID is ready for download.", + response_description="Boolean indicating if the storage is ready.", +) +def is_ready( + storage_request_id: UUID, + short_term_storage_monitor: ShortTermStorageMonitor = depends(ShortTermStorageMonitor), # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 +) -> bool: + storage_target = short_term_storage_monitor.recover_target(storage_request_id) + return short_term_storage_monitor.is_ready(storage_target) - @router.get( - "/api/short_term_storage/{storage_request_id}/ready", - summary="Determine if specified storage request ID is ready for download.", - response_description="Boolean indicating if the storage is ready.", - ) - def is_ready(self, storage_request_id: UUID) -> bool: - storage_target = self.short_term_storage_monitor.recover_target(storage_request_id) - return self.short_term_storage_monitor.is_ready(storage_target) - @router.get( - "/api/short_term_storage/{storage_request_id}", - summary="Serve the staged download specified by request ID.", - response_description="Raw contents of the file.", - response_class=GalaxyFileResponse, - responses={ - 200: { - "description": "The archive file containing the History.", - }, - 204: { - "description": "Request was cancelled without an exception condition recorded.", - }, +@router.get( + "/api/short_term_storage/{storage_request_id}", + summary="Serve the staged download specified by request ID.", + response_description="Raw contents of the file.", + response_class=GalaxyFileResponse, + responses={ + 200: { + "description": "The archive file containing the History.", + }, + 204: { + "description": "Request was cancelled without an exception condition recorded.", }, - ) - def serve(self, storage_request_id: UUID): - storage_target = self.short_term_storage_monitor.recover_target(storage_request_id) - serve_info = self.short_term_storage_monitor.get_serve_info(storage_target) - if isinstance(serve_info, ShortTermStorageServeCompletedInformation): - return GalaxyFileResponse( - path=serve_info.target.path, - media_type=serve_info.mime_type, - filename=serve_info.filename, - ) + }, +) +def serve( + storage_request_id: UUID, + short_term_storage_monitor: ShortTermStorageMonitor = depends(ShortTermStorageMonitor), # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 +): + storage_target = short_term_storage_monitor.recover_target(storage_request_id) + serve_info = short_term_storage_monitor.get_serve_info(storage_target) + if isinstance(serve_info, ShortTermStorageServeCompletedInformation): + return GalaxyFileResponse( + path=serve_info.target.path, + media_type=serve_info.mime_type, + filename=serve_info.filename, + ) - assert isinstance(serve_info, ShortTermStorageServeCancelledInformation) - raise serve_info.message_exception + assert isinstance(serve_info, ShortTermStorageServeCancelledInformation) + raise serve_info.message_exception diff --git a/lib/galaxy/webapps/galaxy/api/storage_cleaner.py b/lib/galaxy/webapps/galaxy/api/storage_cleaner.py index 34a382a8629c..ab7f1cb6615d 100644 --- a/lib/galaxy/webapps/galaxy/api/storage_cleaner.py +++ b/lib/galaxy/webapps/galaxy/api/storage_cleaner.py @@ -46,99 +46,106 @@ ) -@router.cbv -class FastAPIStorageCleaner: - service: StorageCleanerService = depends(StorageCleanerService) - - @router.get( - "/api/storage/histories/discarded/summary", - summary="Returns information with the total storage space taken by discarded histories associated with the given user.", - ) - def discarded_histories_summary( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> CleanableItemsSummary: - return self.service.get_discarded_summary(trans, stored_item_type="history") - - @router.get( - "/api/storage/histories/discarded", - summary="Returns all discarded histories associated with the given user.", - ) - def discarded_histories( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - offset: Optional[int] = OffsetQueryParam, - limit: Optional[int] = LimitQueryParam, - order: Optional[StoredItemOrderBy] = OrderQueryParam, - ) -> List[StoredItem]: - return self.service.get_discarded(trans, "history", offset, limit, order) - - @router.delete( - "/api/storage/histories", - summary="Purges a set of histories by ID. The histories must be owned by the user.", - ) - def cleanup_histories( - self, trans: ProvidesHistoryContext = DependsOnTrans, payload: CleanupStorageItemsRequest = Body(...) - ) -> StorageItemsCleanupResult: - """ - **Warning**: This operation cannot be undone. All objects will be deleted permanently from the disk. - """ - return self.service.cleanup_items(trans, stored_item_type="history", item_ids=set(payload.item_ids)) - - @router.get( - "/api/storage/datasets/discarded/summary", - summary="Returns information with the total storage space taken by discarded datasets owned by the given user.", - ) - def discarded_datasets_summary( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> CleanableItemsSummary: - return self.service.get_discarded_summary(trans, stored_item_type="dataset") - - @router.get( - "/api/storage/datasets/discarded", - summary="Returns discarded datasets owned by the given user. The results can be paginated.", - ) - def discarded_datasets( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - offset: Optional[int] = OffsetQueryParam, - limit: Optional[int] = LimitQueryParam, - order: Optional[StoredItemOrderBy] = OrderQueryParam, - ) -> List[StoredItem]: - return self.service.get_discarded(trans, "dataset", offset, limit, order) - - @router.delete( - "/api/storage/datasets", - summary="Purges a set of datasets by ID from disk. The datasets must be owned by the user.", - ) - def cleanup_datasets( - self, trans: ProvidesHistoryContext = DependsOnTrans, payload: CleanupStorageItemsRequest = Body(...) - ) -> StorageItemsCleanupResult: - """ - **Warning**: This operation cannot be undone. All objects will be deleted permanently from the disk. - """ - return self.service.cleanup_items(trans, stored_item_type="dataset", item_ids=set(payload.item_ids)) - - @router.get( - "/api/storage/histories/archived/summary", - summary="Returns information with the total storage space taken by non-purged archived histories associated with the given user.", - ) - def archived_histories_summary( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - ) -> CleanableItemsSummary: - return self.service.get_archived_summary(trans, stored_item_type="history") - - @router.get( - "/api/storage/histories/archived", - summary="Returns archived histories owned by the given user that are not purged. The results can be paginated.", - ) - def archived_histories( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - offset: Optional[int] = OffsetQueryParam, - limit: Optional[int] = LimitQueryParam, - order: Optional[StoredItemOrderBy] = OrderQueryParam, - ) -> List[StoredItem]: - return self.service.get_archived(trans, "history", offset, limit, order) +@router.get( + "/api/storage/histories/discarded/summary", + summary="Returns information with the total storage space taken by discarded histories associated with the given user.", +) +def discarded_histories_summary( + trans: ProvidesHistoryContext = DependsOnTrans, + service: StorageCleanerService = depends(StorageCleanerService), +) -> CleanableItemsSummary: + return service.get_discarded_summary(trans, stored_item_type="history") + + +@router.get( + "/api/storage/histories/discarded", + summary="Returns all discarded histories associated with the given user.", +) +def discarded_histories( + trans: ProvidesHistoryContext = DependsOnTrans, + offset: Optional[int] = OffsetQueryParam, + limit: Optional[int] = LimitQueryParam, + order: Optional[StoredItemOrderBy] = OrderQueryParam, + service: StorageCleanerService = depends(StorageCleanerService), +) -> List[StoredItem]: + return service.get_discarded(trans, "history", offset, limit, order) + + +@router.delete( + "/api/storage/histories", + summary="Purges a set of histories by ID. The histories must be owned by the user.", +) +def cleanup_histories( + trans: ProvidesHistoryContext = DependsOnTrans, + payload: CleanupStorageItemsRequest = Body(...), + service: StorageCleanerService = depends(StorageCleanerService), +) -> StorageItemsCleanupResult: + """ + **Warning**: This operation cannot be undone. All objects will be deleted permanently from the disk. + """ + return service.cleanup_items(trans, stored_item_type="history", item_ids=set(payload.item_ids)) + + +@router.get( + "/api/storage/datasets/discarded/summary", + summary="Returns information with the total storage space taken by discarded datasets owned by the given user.", +) +def discarded_datasets_summary( + trans: ProvidesHistoryContext = DependsOnTrans, + service: StorageCleanerService = depends(StorageCleanerService), +) -> CleanableItemsSummary: + return service.get_discarded_summary(trans, stored_item_type="dataset") + + +@router.get( + "/api/storage/datasets/discarded", + summary="Returns discarded datasets owned by the given user. The results can be paginated.", +) +def discarded_datasets( + trans: ProvidesHistoryContext = DependsOnTrans, + offset: Optional[int] = OffsetQueryParam, + limit: Optional[int] = LimitQueryParam, + order: Optional[StoredItemOrderBy] = OrderQueryParam, + service: StorageCleanerService = depends(StorageCleanerService), +) -> List[StoredItem]: + return service.get_discarded(trans, "dataset", offset, limit, order) + + +@router.delete( + "/api/storage/datasets", + summary="Purges a set of datasets by ID from disk. The datasets must be owned by the user.", +) +def cleanup_datasets( + trans: ProvidesHistoryContext = DependsOnTrans, + payload: CleanupStorageItemsRequest = Body(...), + service: StorageCleanerService = depends(StorageCleanerService), +) -> StorageItemsCleanupResult: + """ + **Warning**: This operation cannot be undone. All objects will be deleted permanently from the disk. + """ + return service.cleanup_items(trans, stored_item_type="dataset", item_ids=set(payload.item_ids)) + + +@router.get( + "/api/storage/histories/archived/summary", + summary="Returns information with the total storage space taken by non-purged archived histories associated with the given user.", +) +def archived_histories_summary( + trans: ProvidesHistoryContext = DependsOnTrans, + service: StorageCleanerService = depends(StorageCleanerService), +) -> CleanableItemsSummary: + return service.get_archived_summary(trans, stored_item_type="history") + + +@router.get( + "/api/storage/histories/archived", + summary="Returns archived histories owned by the given user that are not purged. The results can be paginated.", +) +def archived_histories( + trans: ProvidesHistoryContext = DependsOnTrans, + offset: Optional[int] = OffsetQueryParam, + limit: Optional[int] = LimitQueryParam, + order: Optional[StoredItemOrderBy] = OrderQueryParam, + service: StorageCleanerService = depends(StorageCleanerService), +) -> List[StoredItem]: + return service.get_archived(trans, "history", offset, limit, order) diff --git a/lib/galaxy/webapps/galaxy/api/tags.py b/lib/galaxy/webapps/galaxy/api/tags.py index 81f8b9f1c0d6..e51d9802ea2d 100644 --- a/lib/galaxy/webapps/galaxy/api/tags.py +++ b/lib/galaxy/webapps/galaxy/api/tags.py @@ -25,28 +25,24 @@ router = Router(tags=["tags"]) -@router.cbv -class FastAPITags: - manager: TagsManager = depends(TagsManager) - - @router.put( - "/api/tags", - summary="Apply a new set of tags to an item.", - status_code=status.HTTP_204_NO_CONTENT, - ) - def update( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: ItemTagsPayload = Body( - ..., # Required - title="Payload", - description="Request body containing the item and the tags to be assigned.", - ), - ): - """Replaces the tags associated with an item with the new ones specified in the payload. - - - The previous tags will be __deleted__. - - If no tags are provided in the request body, the currently associated tags will also be __deleted__. - """ - self.manager.update(trans, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) +@router.put( + "/api/tags", + summary="Apply a new set of tags to an item.", + status_code=status.HTTP_204_NO_CONTENT, +) +def update( + trans: ProvidesUserContext = DependsOnTrans, + payload: ItemTagsPayload = Body( + ..., # Required + title="Payload", + description="Request body containing the item and the tags to be assigned.", + ), + manager: TagsManager = depends(TagsManager), +): + """Replaces the tags associated with an item with the new ones specified in the payload. + + - The previous tags will be __deleted__. + - If no tags are provided in the request body, the currently associated tags will also be __deleted__. + """ + manager.update(trans, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/lib/galaxy/webapps/galaxy/api/tasks.py b/lib/galaxy/webapps/galaxy/api/tasks.py index 158222b1bec0..0346ccca5104 100644 --- a/lib/galaxy/webapps/galaxy/api/tasks.py +++ b/lib/galaxy/webapps/galaxy/api/tasks.py @@ -18,14 +18,10 @@ router = Router(tags=["tasks"]) -@router.cbv -class FastAPITasks: - manager: AsyncTasksManager = depends(AsyncTasksManager) # type: ignore[type-abstract] - - @router.get( - "/api/tasks/{task_id}/state", - summary="Determine state of task ID", - response_description="String indicating task state.", - ) - def state(self, task_id: UUID) -> TaskState: - return self.manager.get_state(task_id) +@router.get( + "/api/tasks/{task_id}/state", + summary="Determine state of task ID", + response_description="String indicating task state.", +) +def state(task_id: UUID, manager: AsyncTasksManager = depends(AsyncTasksManager)) -> TaskState: # type: ignore[type-abstract] + return manager.get_state(task_id) diff --git a/lib/galaxy/webapps/galaxy/api/tool_data.py b/lib/galaxy/webapps/galaxy/api/tool_data.py index 95b8c1f47280..ca3d6f4c80ff 100644 --- a/lib/galaxy/webapps/galaxy/api/tool_data.py +++ b/lib/galaxy/webapps/galaxy/api/tool_data.py @@ -46,98 +46,104 @@ class ImportToolDataBundle(BaseModel): source: ImportToolDataBundleSource = Field(..., discriminator="src") -@router.cbv -class FastAPIToolData: - tool_data_manager: ToolDataManager = depends(ToolDataManager) - - @router.get( - "/api/tool_data", - summary="Lists all available data tables", - response_description="A list with details on individual data tables.", - require_admin=False, - ) - async def index(self) -> ToolDataEntryList: - """Get the list of all available data tables.""" - return self.tool_data_manager.index() - - @router.post( - "/api/tool_data", - summary="Import a data manager bundle", - require_admin=True, - ) - async def create( - self, tool_data_file_path=None, import_bundle_model: ImportToolDataBundle = Body(...) - ) -> AsyncTaskResultSummary: - source = import_bundle_model.source - result = import_data_bundle.delay(tool_data_file_path=tool_data_file_path, **source.dict()) - summary = async_task_summary(result) - return summary - - @router.get( - "/api/tool_data/{table_name}", - summary="Get details of a given data table", - response_description="A description of the given data table and its content", - require_admin=True, - ) - async def show(self, table_name: str = ToolDataTableName) -> ToolDataDetails: - """Get details of a given tool data table.""" - return self.tool_data_manager.show(table_name) - - @router.get( - "/api/tool_data/{table_name}/reload", - summary="Reloads a tool data table", - response_description="A description of the reloaded data table and its content", - require_admin=True, - ) - async def reload(self, table_name: str = ToolDataTableName) -> ToolDataDetails: - """Reloads a data table and return its details.""" - return self.tool_data_manager.reload(table_name) - - @router.get( - "/api/tool_data/{table_name}/fields/{field_name}", - summary="Get information about a particular field in a tool data table", - response_description="Information about a data table field", - require_admin=True, - ) - async def show_field( - self, - table_name: str = ToolDataTableName, - field_name: str = ToolDataTableFieldName, - ) -> ToolDataField: - """Reloads a data table and return its details.""" - return self.tool_data_manager.show_field(table_name, field_name) - - @router.get( - "/api/tool_data/{table_name}/fields/{field_name}/files/{file_name}", - summary="Get information about a particular field in a tool data table", - response_description="Information about a data table field", - response_class=GalaxyFileResponse, - require_admin=True, - ) - async def download_field_file( - self, - table_name: str = ToolDataTableName, - field_name: str = ToolDataTableFieldName, - file_name: str = Path( - ..., # Mark this field as required - title="File name", - description="The name of a file associated with this data table field", - ), - ): - """Download a file associated with the data table field.""" - path = self.tool_data_manager.get_field_file_path(table_name, field_name, file_name) - return GalaxyFileResponse(str(path)) - - @router.delete( - "/api/tool_data/{table_name}", - summary="Removes an item from a data table", - response_description="A description of the affected data table and its content", - require_admin=True, - ) - async def delete( - self, - payload: ToolDataItem, - table_name: str = ToolDataTableName, - ) -> ToolDataDetails: - """Removes an item from a data table and reloads it to return its updated details.""" - return self.tool_data_manager.delete(table_name, payload.values) +@router.get( + "/api/tool_data", + summary="Lists all available data tables", + response_description="A list with details on individual data tables.", + require_admin=False, +) +async def index(tool_data_manager: ToolDataManager = depends(ToolDataManager)) -> ToolDataEntryList: + """Get the list of all available data tables.""" + return tool_data_manager.index() + + +@router.post( + "/api/tool_data", + summary="Import a data manager bundle", + require_admin=True, +) +async def create( + tool_data_file_path=None, import_bundle_model: ImportToolDataBundle = Body(...) +) -> AsyncTaskResultSummary: + source = import_bundle_model.source + result = import_data_bundle.delay(tool_data_file_path=tool_data_file_path, **source.dict()) + summary = async_task_summary(result) + return summary + + +@router.get( + "/api/tool_data/{table_name}", + summary="Get details of a given data table", + response_description="A description of the given data table and its content", + require_admin=True, +) +async def show( + table_name: str = ToolDataTableName, tool_data_manager: ToolDataManager = depends(ToolDataManager) +) -> ToolDataDetails: + """Get details of a given tool data table.""" + return tool_data_manager.show(table_name) + + +@router.get( + "/api/tool_data/{table_name}/reload", + summary="Reloads a tool data table", + response_description="A description of the reloaded data table and its content", + require_admin=True, +) +async def reload( + table_name: str = ToolDataTableName, tool_data_manager: ToolDataManager = depends(ToolDataManager) +) -> ToolDataDetails: + """Reloads a data table and return its details.""" + return tool_data_manager.reload(table_name) + + +@router.get( + "/api/tool_data/{table_name}/fields/{field_name}", + summary="Get information about a particular field in a tool data table", + response_description="Information about a data table field", + require_admin=True, +) +async def show_field( + table_name: str = ToolDataTableName, + field_name: str = ToolDataTableFieldName, + tool_data_manager: ToolDataManager = depends(ToolDataManager), +) -> ToolDataField: + """Reloads a data table and return its details.""" + return tool_data_manager.show_field(table_name, field_name) + + +@router.get( + "/api/tool_data/{table_name}/fields/{field_name}/files/{file_name}", + summary="Get information about a particular field in a tool data table", + response_description="Information about a data table field", + response_class=GalaxyFileResponse, + require_admin=True, +) +async def download_field_file( + table_name: str = ToolDataTableName, + field_name: str = ToolDataTableFieldName, + file_name: str = Path( + ..., # Mark this field as required + title="File name", + description="The name of a file associated with this data table field", + ), + tool_data_manager: ToolDataManager = depends(ToolDataManager), +): + """Download a file associated with the data table field.""" + path = tool_data_manager.get_field_file_path(table_name, field_name, file_name) + return GalaxyFileResponse(str(path)) + + +@router.delete( + "/api/tool_data/{table_name}", + summary="Removes an item from a data table", + response_description="A description of the affected data table and its content", + require_admin=True, +) +async def delete( + payload: ToolDataItem, + table_name: str = ToolDataTableName, + tool_data_manager: ToolDataManager = depends(ToolDataManager), +) -> ToolDataDetails: + """Removes an item from a data table and reloads it to return its updated details.""" + return tool_data_manager.delete(table_name, payload.values) diff --git a/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py b/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py index 88ade5498482..9735df448800 100644 --- a/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py +++ b/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py @@ -405,47 +405,48 @@ def reset_metadata_on_installed_repositories(self, trans, payload, **kwd): ) -@router.cbv -class FastAPIToolShedRepositories: - service: ToolShedRepositoriesService = depends(ToolShedRepositoriesService) - - @router.get( - "/api/tool_shed_repositories", - summary="Lists installed tool shed repositories.", - response_description="A list of installed tool shed repository objects.", +@router.get( + "/api/tool_shed_repositories", + summary="Lists installed tool shed repositories.", + response_description="A list of installed tool shed repository objects.", +) +def index( + name: Optional[str] = NameQueryParam, + owner: Optional[str] = OwnerQueryParam, + changeset: Optional[str] = ChangesetQueryParam, + deleted: Optional[bool] = DeletedQueryParam, + uninstalled: Optional[bool] = UninstalledQueryParam, + service: ToolShedRepositoriesService = depends(ToolShedRepositoriesService), +) -> List[InstalledToolShedRepository]: + request = InstalledToolShedRepositoryIndexRequest( + name=name, + owner=owner, + changeset=changeset, + deleted=deleted, + uninstalled=uninstalled, ) - def index( - self, - name: Optional[str] = NameQueryParam, - owner: Optional[str] = OwnerQueryParam, - changeset: Optional[str] = ChangesetQueryParam, - deleted: Optional[bool] = DeletedQueryParam, - uninstalled: Optional[bool] = UninstalledQueryParam, - ) -> List[InstalledToolShedRepository]: - request = InstalledToolShedRepositoryIndexRequest( - name=name, - owner=owner, - changeset=changeset, - deleted=deleted, - uninstalled=uninstalled, - ) - return self.service.index(request) + return service.index(request) - @router.get( - "/api/tool_shed_repositories/check_for_updates", - summary="Check for updates to the specified repository, or all installed repositories.", - response_description="A description of the state and updates message.", - require_admin=True, - ) - def check_for_updates(self, id: Optional[DecodedDatabaseIdField] = None) -> CheckForUpdatesResponse: - return self.service.check_for_updates(id and int(id)) - @router.get( - "/api/tool_shed_repositories/{id}", - summary="Show installed tool shed repository.", - ) - def show( - self, - id: DecodedDatabaseIdField = InstalledToolShedRepositoryIDPathParam, - ) -> InstalledToolShedRepository: - return self.service.show(id) +@router.get( + "/api/tool_shed_repositories/check_for_updates", + summary="Check for updates to the specified repository, or all installed repositories.", + response_description="A description of the state and updates message.", + require_admin=True, +) +def check_for_updates( + id: Optional[DecodedDatabaseIdField] = None, + service: ToolShedRepositoriesService = depends(ToolShedRepositoriesService), +) -> CheckForUpdatesResponse: + return service.check_for_updates(id and int(id)) + + +@router.get( + "/api/tool_shed_repositories/{id}", + summary="Show installed tool shed repository.", +) +def show( + id: DecodedDatabaseIdField = InstalledToolShedRepositoryIDPathParam, + service: ToolShedRepositoriesService = depends(ToolShedRepositoriesService), +) -> InstalledToolShedRepository: + return service.show(id) diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 38f346bffe8e..d3f88ce40252 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -73,35 +73,36 @@ class JsonApiRoute(APIContentTypeRoute): FetchDataForm = as_form(FetchDataFormPayload) -@router.cbv -class FetchTools: - service: ToolsService = depends(ToolsService) - - @router.post("/api/tools/fetch", summary="Upload files to Galaxy", route_class_override=JsonApiRoute) - async def fetch_json(self, payload: FetchDataPayload = Body(...), trans: ProvidesHistoryContext = DependsOnTrans): - return self.service.create_fetch(trans, payload) - - @router.post( - "/api/tools/fetch", - summary="Upload files to Galaxy", - route_class_override=FormDataApiRoute, - ) - async def fetch_form( - self, - request: Request, - payload: FetchDataFormPayload = Depends(FetchDataForm.as_form), - files: Optional[List[UploadFile]] = None, - trans: ProvidesHistoryContext = DependsOnTrans, - ): - files2: List[StarletteUploadFile] = cast(List[StarletteUploadFile], files or []) - - # FastAPI's UploadFile is a very light wrapper around starlette's UploadFile - if not files2: - data = await request.form() - for value in data.values(): - if isinstance(value, StarletteUploadFile): - files2.append(value) - return self.service.create_fetch(trans, payload, files2) +@router.post("/api/tools/fetch", summary="Upload files to Galaxy", route_class_override=JsonApiRoute) +async def fetch_json( + payload: FetchDataPayload = Body(...), + trans: ProvidesHistoryContext = DependsOnTrans, + service: ToolsService = depends(ToolsService), +): + return service.create_fetch(trans, payload) + + +@router.post( + "/api/tools/fetch", + summary="Upload files to Galaxy", + route_class_override=FormDataApiRoute, +) +async def fetch_form( + request: Request, + payload: FetchDataFormPayload = Depends(FetchDataForm.as_form), + files: Optional[List[UploadFile]] = None, + trans: ProvidesHistoryContext = DependsOnTrans, + service: ToolsService = depends(ToolsService), +): + files2: List[StarletteUploadFile] = cast(List[StarletteUploadFile], files or []) + + # FastAPI's UploadFile is a very light wrapper around starlette's UploadFile + if not files2: + data = await request.form() + for value in data.values(): + if isinstance(value, StarletteUploadFile): + files2.append(value) + return service.create_fetch(trans, payload, files2) class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin): diff --git a/lib/galaxy/webapps/galaxy/api/tours.py b/lib/galaxy/webapps/galaxy/api/tours.py index d1205888bf6e..9a88f8585396 100644 --- a/lib/galaxy/webapps/galaxy/api/tours.py +++ b/lib/galaxy/webapps/galaxy/api/tours.py @@ -19,21 +19,19 @@ router = Router(tags=["tours"]) -@router.cbv -class FastAPITours: - registry: ToursRegistry = depends(ToursRegistry) # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 - - @router.get("/api/tours") - def index(self) -> TourList: - """Return list of available tours.""" - return self.registry.get_tours() - - @router.get("/api/tours/{tour_id}") - def show(self, tour_id: str) -> TourDetails: - """Return a tour definition.""" - return self.registry.tour_contents(tour_id) - - @router.post("/api/tours/{tour_id}", require_admin=True) - def update_tour(self, tour_id: str) -> TourDetails: - """Return a tour definition.""" - return self.registry.load_tour(tour_id) +@router.get("/api/tours") +def index(registry: ToursRegistry = depends(ToursRegistry)) -> TourList: # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 + """Return list of available tours.""" + return registry.get_tours() + + +@router.get("/api/tours/{tour_id}") +def show(tour_id: str, registry: ToursRegistry = depends(ToursRegistry)) -> TourDetails: # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 + """Return a tour definition.""" + return registry.tour_contents(tour_id) + + +@router.post("/api/tours/{tour_id}", require_admin=True) +def update_tour(tour_id: str, registry: ToursRegistry = depends(ToursRegistry)) -> TourDetails: # type: ignore[type-abstract] # https://github.com/python/mypy/issues/4717 + """Return a tour definition.""" + return registry.load_tour(tour_id) diff --git a/lib/galaxy/webapps/galaxy/api/users.py b/lib/galaxy/webapps/galaxy/api/users.py index ae2b0d78a3f6..99258c592c35 100644 --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -148,566 +148,591 @@ AnyUserModel = Union[DetailedUserModel, AnonUserModel] -@router.cbv -class FastAPIUsers: - service: UsersService = depends(UsersService) - user_serializer: users.UserSerializer = depends(users.UserSerializer) - - @router.put( - "/api/users/current/recalculate_disk_usage", - summary=RecalculateDiskUsageSummary, - responses=RecalculateDiskUsageResponseDescriptions, - ) - @router.put( - "/api/users/recalculate_disk_usage", - summary=RecalculateDiskUsageSummary, - responses=RecalculateDiskUsageResponseDescriptions, - deprecated=True, - ) - def recalculate_disk_usage( - self, - trans: ProvidesUserContext = DependsOnTrans, - ): - """This route will be removed in a future version. - - Please use `/api/users/current/recalculate_disk_usage` instead. - """ - user_id = getattr(trans.user, "id", None) - if not user_id: - raise exceptions.AuthenticationRequired("Only registered users can recalculate disk usage.") - else: - result = self.service.recalculate_disk_usage(trans, user_id) - return Response(status_code=status.HTTP_204_NO_CONTENT) if result is None else result - - @router.put( - "/api/users/{user_id}/recalculate_disk_usage", - summary=RecalculateDiskUsageSummary, - responses=RecalculateDiskUsageResponseDescriptions, - require_admin=True, - ) - def recalculate_disk_usage_by_user_id( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ): - result = self.service.recalculate_disk_usage(trans, user_id) +@router.put( + "/api/users/current/recalculate_disk_usage", + summary=RecalculateDiskUsageSummary, + responses=RecalculateDiskUsageResponseDescriptions, +) +@router.put( + "/api/users/recalculate_disk_usage", + summary=RecalculateDiskUsageSummary, + responses=RecalculateDiskUsageResponseDescriptions, + deprecated=True, +) +def recalculate_disk_usage( + trans: ProvidesUserContext = DependsOnTrans, + service: UsersService = depends(UsersService), +): + """This route will be removed in a future version. + + Please use `/api/users/current/recalculate_disk_usage` instead. + """ + user_id = getattr(trans.user, "id", None) + if not user_id: + raise exceptions.AuthenticationRequired("Only registered users can recalculate disk usage.") + else: + result = service.recalculate_disk_usage(trans, user_id) return Response(status_code=status.HTTP_204_NO_CONTENT) if result is None else result - @router.get( - "/api/users/deleted", - name="get_deleted_users", - description="Return a collection of deleted users. Only admins can see deleted users.", - ) - def index_deleted( - self, - trans: ProvidesUserContext = DependsOnTrans, - f_email: Optional[str] = FilterEmailQueryParam, - f_name: Optional[str] = FilterNameQueryParam, - f_any: Optional[str] = FilterAnyQueryParam, - ) -> List[Union[UserModel, LimitedUserModel]]: - return self.service.get_index(trans=trans, deleted=True, f_email=f_email, f_name=f_name, f_any=f_any) - - @router.post( - "/api/users/deleted/{user_id}/undelete", - name="undelete_user", - summary="Restore a deleted user. Only admins can restore users.", - require_admin=True, - ) - def undelete( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ) -> DetailedUserModel: - user = self.service.get_user(trans=trans, user_id=user_id) - self.service.user_manager.undelete(user) - return self.service.user_to_detailed_model(user) - - @router.get( - "/api/users/deleted/{user_id}", - name="get_deleted_user", - summary="Return information about a deleted user. Only admins can see deleted users.", - ) - def show_deleted( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ) -> AnyUserModel: - return self.service.show_user(trans=trans, user_id=user_id, deleted=True) - - @router.get( - "/api/users/{user_id}/api_key", - name="get_or_create_api_key", - summary="Return the user's API key", - ) - def get_or_create_api_key( - self, trans: ProvidesUserContext = DependsOnTrans, user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam - ) -> str: - return self.service.get_or_create_api_key(trans, user_id) - - @router.get( - "/api/users/{user_id}/api_key/detailed", - name="get_api_key_detailed", - summary="Return the user's API key with extra information.", - responses={ - 200: { - "model": APIKeyModel, - "description": "The API key of the user.", - }, - 204: { - "description": "The user doesn't have an API key.", - }, + +@router.put( + "/api/users/{user_id}/recalculate_disk_usage", + summary=RecalculateDiskUsageSummary, + responses=RecalculateDiskUsageResponseDescriptions, + require_admin=True, +) +def recalculate_disk_usage_by_user_id( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +): + result = service.recalculate_disk_usage(trans, user_id) + return Response(status_code=status.HTTP_204_NO_CONTENT) if result is None else result + + +@router.get( + "/api/users/deleted", + name="get_deleted_users", + description="Return a collection of deleted users. Only admins can see deleted users.", +) +def index_deleted( + trans: ProvidesUserContext = DependsOnTrans, + f_email: Optional[str] = FilterEmailQueryParam, + f_name: Optional[str] = FilterNameQueryParam, + f_any: Optional[str] = FilterAnyQueryParam, + service: UsersService = depends(UsersService), +) -> List[Union[UserModel, LimitedUserModel]]: + return service.get_index(trans=trans, deleted=True, f_email=f_email, f_name=f_name, f_any=f_any) + + +@router.post( + "/api/users/deleted/{user_id}/undelete", + name="undelete_user", + summary="Restore a deleted user. Only admins can restore users.", + require_admin=True, +) +def undelete( + trans: ProvidesHistoryContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> DetailedUserModel: + user = service.get_user(trans=trans, user_id=user_id) + service.user_manager.undelete(user) + return service.user_to_detailed_model(user) + + +@router.get( + "/api/users/deleted/{user_id}", + name="get_deleted_user", + summary="Return information about a deleted user. Only admins can see deleted users.", +) +def show_deleted( + trans: ProvidesHistoryContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> AnyUserModel: + return service.show_user(trans=trans, user_id=user_id, deleted=True) + + +@router.get( + "/api/users/{user_id}/api_key", + name="get_or_create_api_key", + summary="Return the user's API key", +) +def get_or_create_api_key( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> str: + return service.get_or_create_api_key(trans, user_id) + + +@router.get( + "/api/users/{user_id}/api_key/detailed", + name="get_api_key_detailed", + summary="Return the user's API key with extra information.", + responses={ + 200: { + "model": APIKeyModel, + "description": "The API key of the user.", }, - ) - def get_api_key( - self, trans: ProvidesUserContext = DependsOnTrans, user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam - ): - api_key = self.service.get_api_key(trans, user_id) - return api_key if api_key else Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.post("/api/users/{user_id}/api_key", name="create_api_key", summary="Create a new API key for the user") - def create_api_key( - self, trans: ProvidesUserContext = DependsOnTrans, user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam - ) -> str: - return self.service.create_api_key(trans, user_id).key - - @router.delete( - "/api/users/{user_id}/api_key", - name="delete_api_key", - summary="Delete the current API key of the user", - status_code=status.HTTP_204_NO_CONTENT, - ) - def delete_api_key( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ): - self.service.delete_api_key(trans, user_id) - return Response(status_code=status.HTTP_204_NO_CONTENT) + 204: { + "description": "The user doesn't have an API key.", + }, + }, +) +def get_api_key( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +): + api_key = service.get_api_key(trans, user_id) + return api_key if api_key else Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post("/api/users/{user_id}/api_key", name="create_api_key", summary="Create a new API key for the user") +def create_api_key( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> str: + return service.create_api_key(trans, user_id).key + + +@router.delete( + "/api/users/{user_id}/api_key", + name="delete_api_key", + summary="Delete the current API key of the user", + status_code=status.HTTP_204_NO_CONTENT, +) +def delete_api_key( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +): + service.delete_api_key(trans, user_id) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/api/users/{user_id}/usage", + name="get_user_usage", + summary="Return the user's quota usage summary broken down by quota source", +) +def usage( + trans: ProvidesUserContext = DependsOnTrans, + user_id: FlexibleUserIdType = FlexibleUserIdPathParam, + service: UsersService = depends(UsersService), + user_serializer: users.UserSerializer = depends(users.UserSerializer), +) -> List[UserQuotaUsage]: + if user := service.get_user_full(trans, user_id, False): + rval = user_serializer.serialize_disk_usage(user) + return rval + else: + return [] + + +@router.get( + "/api/users/{user_id}/usage/{label}", + name="get_user_usage_for_label", + summary="Return the user's quota usage summary for a given quota source label", +) +def usage_for( + trans: ProvidesUserContext = DependsOnTrans, + user_id: FlexibleUserIdType = FlexibleUserIdPathParam, + label: str = QuotaSourceLabelPathParam, + service: UsersService = depends(UsersService), + user_serializer: users.UserSerializer = depends(users.UserSerializer), +) -> Optional[UserQuotaUsage]: + effective_label: Optional[str] = label + if label == "__null__": + effective_label = None + if user := service.get_user_full(trans, user_id, False): + rval = user_serializer.serialize_disk_usage_for(user, effective_label) + return rval + else: + return None + + +@router.get( + "/api/users/{user_id}/beacon", + name="get_beacon_settings", + summary="Return information about beacon share settings", +) +def get_beacon( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> UserBeaconSetting: + """ + **Warning**: This endpoint is experimental and might change or disappear in future versions. + """ + user = service.get_user(trans, user_id) - @router.get( - "/api/users/{user_id}/usage", - name="get_user_usage", - summary="Return the user's quota usage summary broken down by quota source", - ) - def usage( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: FlexibleUserIdType = FlexibleUserIdPathParam, - ) -> List[UserQuotaUsage]: - if user := self.service.get_user_full(trans, user_id, False): - rval = self.user_serializer.serialize_disk_usage(user) - return rval - else: - return [] + enabled = user.preferences["beacon_enabled"] if "beacon_enabled" in user.preferences else False - @router.get( - "/api/users/{user_id}/usage/{label}", - name="get_user_usage_for_label", - summary="Return the user's quota usage summary for a given quota source label", - ) - def usage_for( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: FlexibleUserIdType = FlexibleUserIdPathParam, - label: str = QuotaSourceLabelPathParam, - ) -> Optional[UserQuotaUsage]: - effective_label: Optional[str] = label - if label == "__null__": - effective_label = None - if user := self.service.get_user_full(trans, user_id, False): - rval = self.user_serializer.serialize_disk_usage_for(user, effective_label) - return rval - else: - return None - - @router.get( - "/api/users/{user_id}/beacon", - name="get_beacon_settings", - summary="Return information about beacon share settings", - ) - def get_beacon( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ) -> UserBeaconSetting: - """ - **Warning**: This endpoint is experimental and might change or disappear in future versions. - """ - user = self.service.get_user(trans, user_id) - - enabled = user.preferences["beacon_enabled"] if "beacon_enabled" in user.preferences else False - - return UserBeaconSetting(enabled=enabled) - - @router.post( - "/api/users/{user_id}/beacon", - name="set_beacon_settings", - summary="Change beacon setting", - ) - def set_beacon( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - payload: UserBeaconSetting = Body(...), - ) -> UserBeaconSetting: - """ - **Warning**: This endpoint is experimental and might change or disappear in future versions. - """ - user = self.service.get_user(trans, user_id) + return UserBeaconSetting(enabled=enabled) - user.preferences["beacon_enabled"] = payload.enabled - with transaction(trans.sa_session): - trans.sa_session.commit() - return payload - - @router.delete( - "/api/users/{user_id}/favorites/{object_type}/{object_id}", - name="remove_favorite", - summary="Remove the object from user's favorites", - ) - def remove_favorite( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - object_type: FavoriteObjectType = ObjectTypePathParam, - object_id: str = ObjectIDPathParam, - ) -> FavoriteObjectsSummary: - user = self.service.get_user(trans, user_id) - favorites = json.loads(user.preferences["favorites"]) if "favorites" in user.preferences else {} - if object_type.value == "tools": - if "tools" in favorites: - favorite_tools = favorites["tools"] - if object_id in favorite_tools: - del favorite_tools[favorite_tools.index(object_id)] - favorites["tools"] = favorite_tools - user.preferences["favorites"] = json.dumps(favorites) - with transaction(trans.sa_session): - trans.sa_session.commit() - else: - raise exceptions.ObjectNotFound("Given object is not in the list of favorites") - return favorites - - @router.put( - "/api/users/{user_id}/favorites/{object_type}", - name="set_favorite", - summary="Add the object to user's favorites", - ) - def set_favorite( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - object_type: FavoriteObjectType = ObjectTypePathParam, - payload: FavoriteObject = FavoriteObjectBody, - ) -> FavoriteObjectsSummary: - user = self.service.get_user(trans, user_id) - favorites = json.loads(user.preferences["favorites"]) if "favorites" in user.preferences else {} - if object_type.value == "tools": - tool_id = payload.object_id - tool = trans.app.toolbox.get_tool(tool_id) - if not tool: - raise exceptions.ObjectNotFound(f"Could not find tool with id '{tool_id}'.") - if not tool.allow_user_access(user): - raise exceptions.AuthenticationFailed(f"Access denied for tool with id '{tool_id}'.") - if "tools" in favorites: - favorite_tools = favorites["tools"] - else: - favorite_tools = [] - if tool_id not in favorite_tools: - favorite_tools.append(tool_id) +@router.post( + "/api/users/{user_id}/beacon", + name="set_beacon_settings", + summary="Change beacon setting", +) +def set_beacon( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + payload: UserBeaconSetting = Body(...), + service: UsersService = depends(UsersService), +) -> UserBeaconSetting: + """ + **Warning**: This endpoint is experimental and might change or disappear in future versions. + """ + user = service.get_user(trans, user_id) + + user.preferences["beacon_enabled"] = payload.enabled + with transaction(trans.sa_session): + trans.sa_session.commit() + + return payload + + +@router.delete( + "/api/users/{user_id}/favorites/{object_type}/{object_id}", + name="remove_favorite", + summary="Remove the object from user's favorites", +) +def remove_favorite( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + object_type: FavoriteObjectType = ObjectTypePathParam, + object_id: str = ObjectIDPathParam, + service: UsersService = depends(UsersService), +) -> FavoriteObjectsSummary: + user = service.get_user(trans, user_id) + favorites = json.loads(user.preferences["favorites"]) if "favorites" in user.preferences else {} + if object_type.value == "tools": + if "tools" in favorites: + favorite_tools = favorites["tools"] + if object_id in favorite_tools: + del favorite_tools[favorite_tools.index(object_id)] favorites["tools"] = favorite_tools user.preferences["favorites"] = json.dumps(favorites) with transaction(trans.sa_session): trans.sa_session.commit() - return favorites - - @router.put( - "/api/users/{user_id}/theme/{theme}", - name="set_theme", - summary="Set the user's theme choice", - ) - def set_theme( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - theme: str = ThemePathParam, - ) -> str: - user = self.service.get_user(trans, user_id) - user.preferences["theme"] = theme - with transaction(trans.sa_session): - trans.sa_session.commit() - return theme - - @router.put( - "/api/users/{user_id}/custom_builds/{key}", - name="add_custom_builds", - summary="Add new custom build.", - ) - def add_custom_builds( - self, - key: str = CustomBuildKeyPathParam, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - payload: CustomBuildCreationPayload = CustomBuildCreationBody, - ) -> Any: - user = self.service.get_user(trans, user_id) - dbkeys = json.loads(user.preferences["dbkeys"]) if "dbkeys" in user.preferences else {} - name = payload.name - len_type = payload.len_type - len_value = payload.len_value - if len_type not in ["file", "fasta", "text"] or not len_value: - raise exceptions.RequestParameterInvalidException("Please specify a valid data source type.") - if not name or not key: - raise exceptions.RequestParameterMissingException("You must specify values for all the fields.") - elif key in dbkeys: - raise exceptions.DuplicatedIdentifierException( - "There is already a custom build with that key. Delete it first if you want to replace it." - ) - else: - # Have everything needed; create new build. - build_dict = {"name": name} - if len_type in ["text", "file"]: - # Create new len file - new_len = trans.app.model.HistoryDatasetAssociation( - extension="len", create_dataset=True, sa_session=trans.sa_session - ) - trans.sa_session.add(new_len) - new_len.name = name - new_len.visible = False - new_len.state = trans.app.model.Job.states.OK - new_len.info = "custom build .len file" - try: - trans.app.object_store.create(new_len.dataset) - except ObjectInvalid: - raise exceptions.InternalServerError("Unable to create output dataset: object store is full.") - with transaction(trans.sa_session): - trans.sa_session.commit() - counter = 0 - lines_skipped = 0 - with open(new_len.get_file_name(), "w") as f: - # LEN files have format: - # - for line in len_value.split("\n"): - # Splits at the last whitespace in the line - lst = line.strip().rsplit(None, 1) - if not lst or len(lst) < 2: - lines_skipped += 1 - continue - # TODO Does name length_str fit here? - chrom, length_str = lst[0], lst[1] - try: - length = int(length_str) - except ValueError: - lines_skipped += 1 - continue - if chrom != escape(chrom): - build_dict["message"] = "Invalid chromosome(s) with HTML detected and skipped." - lines_skipped += 1 - continue - counter += 1 - f.write(f"{chrom}\t{length}\n") - build_dict["len"] = new_len.id - build_dict["count"] = str(counter) else: - build_dict["fasta"] = trans.security.decode_id(len_value) - dataset = trans.sa_session.get(HistoryDatasetAssociation, int(build_dict["fasta"])) - try: - new_len = dataset.get_converted_dataset(trans, "len") - new_linecount = new_len.get_converted_dataset(trans, "linecount") - build_dict["len"] = new_len.id - build_dict["linecount"] = new_linecount.id - except Exception: - raise exceptions.ToolExecutionError("Failed to convert dataset.") - dbkeys[key] = build_dict - user.preferences["dbkeys"] = json.dumps(dbkeys) + raise exceptions.ObjectNotFound("Given object is not in the list of favorites") + return favorites + + +@router.put( + "/api/users/{user_id}/favorites/{object_type}", + name="set_favorite", + summary="Add the object to user's favorites", +) +def set_favorite( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + object_type: FavoriteObjectType = ObjectTypePathParam, + payload: FavoriteObject = FavoriteObjectBody, + service: UsersService = depends(UsersService), +) -> FavoriteObjectsSummary: + user = service.get_user(trans, user_id) + favorites = json.loads(user.preferences["favorites"]) if "favorites" in user.preferences else {} + if object_type.value == "tools": + tool_id = payload.object_id + tool = trans.app.toolbox.get_tool(tool_id) + if not tool: + raise exceptions.ObjectNotFound(f"Could not find tool with id '{tool_id}'.") + if not tool.allow_user_access(user): + raise exceptions.AuthenticationFailed(f"Access denied for tool with id '{tool_id}'.") + if "tools" in favorites: + favorite_tools = favorites["tools"] + else: + favorite_tools = [] + if tool_id not in favorite_tools: + favorite_tools.append(tool_id) + favorites["tools"] = favorite_tools + user.preferences["favorites"] = json.dumps(favorites) with transaction(trans.sa_session): trans.sa_session.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.get( - "/api/users/{user_id}/custom_builds", name="get_custom_builds", summary=" Returns collection of custom builds." - ) - def get_custom_builds( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ) -> CustomBuildsCollection: - user = self.service.get_user(trans, user_id) - dbkeys = json.loads(user.preferences["dbkeys"]) if "dbkeys" in user.preferences else {} - valid_dbkeys = {} - update = False - for key, dbkey in dbkeys.items(): - if "count" not in dbkey and "linecount" in dbkey: - chrom_count_dataset = trans.sa_session.get(HistoryDatasetAssociation, dbkey["linecount"]) - if ( - chrom_count_dataset - and not chrom_count_dataset.deleted - and chrom_count_dataset.state == trans.app.model.HistoryDatasetAssociation.states.OK - ): - chrom_count = int(open(chrom_count_dataset.get_file_name()).readline()) - dbkey["count"] = chrom_count - valid_dbkeys[key] = dbkey - update = True - else: - valid_dbkeys[key] = dbkey - if update: - user.preferences["dbkeys"] = json.dumps(valid_dbkeys) - dbkey_collection = [] - for key, attributes in valid_dbkeys.items(): - attributes["id"] = key - dbkey_collection.append(attributes) - return CustomBuildsCollection.construct(__root__=dbkey_collection) - - @router.delete( - "/api/users/{user_id}/custom_builds/{key}", name="delete_custom_build", summary="Delete a custom build" - ) - def delete_custom_builds( - self, - key: str = CustomBuildKeyPathParam, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ) -> DeletedCustomBuild: - user = self.service.get_user(trans, user_id) - dbkeys = json.loads(user.preferences["dbkeys"]) if "dbkeys" in user.preferences else {} - if key and key in dbkeys: - del dbkeys[key] - user.preferences["dbkeys"] = json.dumps(dbkeys) + return favorites + + +@router.put( + "/api/users/{user_id}/theme/{theme}", + name="set_theme", + summary="Set the user's theme choice", +) +def set_theme( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + theme: str = ThemePathParam, + service: UsersService = depends(UsersService), +) -> str: + user = service.get_user(trans, user_id) + user.preferences["theme"] = theme + with transaction(trans.sa_session): + trans.sa_session.commit() + return theme + + +@router.put( + "/api/users/{user_id}/custom_builds/{key}", + name="add_custom_builds", + summary="Add new custom build.", +) +def add_custom_builds( + key: str = CustomBuildKeyPathParam, + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + payload: CustomBuildCreationPayload = CustomBuildCreationBody, + service: UsersService = depends(UsersService), +) -> Any: + user = service.get_user(trans, user_id) + dbkeys = json.loads(user.preferences["dbkeys"]) if "dbkeys" in user.preferences else {} + name = payload.name + len_type = payload.len_type + len_value = payload.len_value + if len_type not in ["file", "fasta", "text"] or not len_value: + raise exceptions.RequestParameterInvalidException("Please specify a valid data source type.") + if not name or not key: + raise exceptions.RequestParameterMissingException("You must specify values for all the fields.") + elif key in dbkeys: + raise exceptions.DuplicatedIdentifierException( + "There is already a custom build with that key. Delete it first if you want to replace it." + ) + else: + # Have everything needed; create new build. + build_dict = {"name": name} + if len_type in ["text", "file"]: + # Create new len file + new_len = trans.app.model.HistoryDatasetAssociation( + extension="len", create_dataset=True, sa_session=trans.sa_session + ) + trans.sa_session.add(new_len) + new_len.name = name + new_len.visible = False + new_len.state = trans.app.model.Job.states.OK + new_len.info = "custom build .len file" + try: + trans.app.object_store.create(new_len.dataset) + except ObjectInvalid: + raise exceptions.InternalServerError("Unable to create output dataset: object store is full.") with transaction(trans.sa_session): trans.sa_session.commit() - return DeletedCustomBuild(message=f"Deleted {key}.") + counter = 0 + lines_skipped = 0 + with open(new_len.get_file_name(), "w") as f: + # LEN files have format: + # + for line in len_value.split("\n"): + # Splits at the last whitespace in the line + lst = line.strip().rsplit(None, 1) + if not lst or len(lst) < 2: + lines_skipped += 1 + continue + # TODO Does name length_str fit here? + chrom, length_str = lst[0], lst[1] + try: + length = int(length_str) + except ValueError: + lines_skipped += 1 + continue + if chrom != escape(chrom): + build_dict["message"] = "Invalid chromosome(s) with HTML detected and skipped." + lines_skipped += 1 + continue + counter += 1 + f.write(f"{chrom}\t{length}\n") + build_dict["len"] = new_len.id + build_dict["count"] = str(counter) else: - raise exceptions.ObjectNotFound(f"Could not find and delete build ({key}).") - - @router.post( - "/api/users", - name="create_user", - summary="Create a new Galaxy user. Only admins can create users for now.", - ) - def create( - self, - trans: ProvidesUserContext = DependsOnTrans, - payload: Union[UserCreationPayload, RemoteUserCreationPayload] = UserCreationBody, - ) -> CreatedUserModel: - if isinstance(payload, UserCreationPayload): - email = payload.email - username = payload.username - password = payload.password - if isinstance(payload, RemoteUserCreationPayload): - email = payload.remote_user_email - username = "" - password = "" - if not trans.app.config.allow_user_creation and not trans.user_is_admin: - raise exceptions.ConfigDoesNotAllowException("User creation is not allowed in this Galaxy instance") - if trans.app.config.use_remote_user and trans.user_is_admin: - user = self.service.user_manager.get_or_create_remote_user(remote_user_email=email) - elif trans.user_is_admin: - message = "\n".join( - ( - validate_email(trans, email), - validate_password(trans, password, password), - validate_publicname(trans, username), - ) - ).rstrip() - if message: - raise exceptions.RequestParameterInvalidException(message) - else: - user = self.service.user_manager.create(email=email, username=username, password=password) + build_dict["fasta"] = trans.security.decode_id(len_value) + dataset = trans.sa_session.get(HistoryDatasetAssociation, int(build_dict["fasta"])) + try: + new_len = dataset.get_converted_dataset(trans, "len") + new_linecount = new_len.get_converted_dataset(trans, "linecount") + build_dict["len"] = new_len.id + build_dict["linecount"] = new_linecount.id + except Exception: + raise exceptions.ToolExecutionError("Failed to convert dataset.") + dbkeys[key] = build_dict + user.preferences["dbkeys"] = json.dumps(dbkeys) + with transaction(trans.sa_session): + trans.sa_session.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/api/users/{user_id}/custom_builds", name="get_custom_builds", summary=" Returns collection of custom builds." +) +def get_custom_builds( + trans: ProvidesHistoryContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> CustomBuildsCollection: + user = service.get_user(trans, user_id) + dbkeys = json.loads(user.preferences["dbkeys"]) if "dbkeys" in user.preferences else {} + valid_dbkeys = {} + update = False + for key, dbkey in dbkeys.items(): + if "count" not in dbkey and "linecount" in dbkey: + chrom_count_dataset = trans.sa_session.get(HistoryDatasetAssociation, dbkey["linecount"]) + if ( + chrom_count_dataset + and not chrom_count_dataset.deleted + and chrom_count_dataset.state == trans.app.model.HistoryDatasetAssociation.states.OK + ): + chrom_count = int(open(chrom_count_dataset.get_file_name()).readline()) + dbkey["count"] = chrom_count + valid_dbkeys[key] = dbkey + update = True else: - raise exceptions.NotImplemented() - item = user.to_dict(view="element", value_mapper={"id": trans.security.encode_id, "total_disk_usage": float}) - return item - - @router.get( - "/api/users", - name="get_users", - description="Return a collection of users. Filters will only work if enabled in config or user is admin.", - response_model_exclude_unset=True, - ) - def index( - self, - trans: ProvidesUserContext = DependsOnTrans, - deleted: bool = UsersDeletedQueryParam, - f_email: Optional[str] = FilterEmailQueryParam, - f_name: Optional[str] = FilterNameQueryParam, - f_any: Optional[str] = FilterAnyQueryParam, - ) -> List[Union[UserModel, LimitedUserModel]]: - return self.service.get_index(trans=trans, deleted=deleted, f_email=f_email, f_name=f_name, f_any=f_any) - - @router.get( - "/api/users/{user_id}", - name="get_user", - summary="Return information about a specified or the current user. Only admin can see deleted or other users", - ) - def show( - self, - trans: ProvidesHistoryContext = DependsOnTrans, - user_id: FlexibleUserIdType = FlexibleUserIdPathParam, - deleted: Optional[bool] = UserDeletedQueryParam, - ) -> AnyUserModel: - user_deleted = deleted or False - return self.service.show_user(trans=trans, user_id=user_id, deleted=user_deleted) - - @router.put( - "/api/users/{user_id}", name="update_user", summary="Update the values of a user. Only admin can update others." - ) - def update( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: FlexibleUserIdType = FlexibleUserIdPathParam, - payload: Dict[Any, Any] = UserUpdateBody, - deleted: Optional[bool] = UserDeletedQueryParam, - ) -> DetailedUserModel: - deleted = deleted or False - current_user = trans.user - user_to_update = self.service.get_non_anonymous_user_full(trans, user_id, deleted=deleted) - self.service.user_deserializer.deserialize(user_to_update, payload, user=current_user, trans=trans) - return self.service.user_to_detailed_model(user_to_update) - - @router.delete( - "/api/users/{user_id}", - name="delete_user", - summary="Delete a user. Only admins can delete others or purge users.", - ) - def delete( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - payload: Optional[UserDeletionPayload] = UserDeletionBody, - ) -> DetailedUserModel: - user_to_update = self.service.user_manager.by_id(user_id) - if payload: - purge = payload.purge + valid_dbkeys[key] = dbkey + if update: + user.preferences["dbkeys"] = json.dumps(valid_dbkeys) + dbkey_collection = [] + for key, attributes in valid_dbkeys.items(): + attributes["id"] = key + dbkey_collection.append(attributes) + return CustomBuildsCollection.construct(__root__=dbkey_collection) + + +@router.delete("/api/users/{user_id}/custom_builds/{key}", name="delete_custom_build", summary="Delete a custom build") +def delete_custom_builds( + key: str = CustomBuildKeyPathParam, + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +) -> DeletedCustomBuild: + user = service.get_user(trans, user_id) + dbkeys = json.loads(user.preferences["dbkeys"]) if "dbkeys" in user.preferences else {} + if key and key in dbkeys: + del dbkeys[key] + user.preferences["dbkeys"] = json.dumps(dbkeys) + with transaction(trans.sa_session): + trans.sa_session.commit() + return DeletedCustomBuild(message=f"Deleted {key}.") + else: + raise exceptions.ObjectNotFound(f"Could not find and delete build ({key}).") + + +@router.post( + "/api/users", + name="create_user", + summary="Create a new Galaxy user. Only admins can create users for now.", +) +def create( + trans: ProvidesUserContext = DependsOnTrans, + payload: Union[UserCreationPayload, RemoteUserCreationPayload] = UserCreationBody, + service: UsersService = depends(UsersService), +) -> CreatedUserModel: + if isinstance(payload, UserCreationPayload): + email = payload.email + username = payload.username + password = payload.password + if isinstance(payload, RemoteUserCreationPayload): + email = payload.remote_user_email + username = "" + password = "" + if not trans.app.config.allow_user_creation and not trans.user_is_admin: + raise exceptions.ConfigDoesNotAllowException("User creation is not allowed in this Galaxy instance") + if trans.app.config.use_remote_user and trans.user_is_admin: + user = service.user_manager.get_or_create_remote_user(remote_user_email=email) + elif trans.user_is_admin: + message = "\n".join( + ( + validate_email(trans, email), + validate_password(trans, password, password), + validate_publicname(trans, username), + ) + ).rstrip() + if message: + raise exceptions.RequestParameterInvalidException(message) else: - purge = False - if trans.user_is_admin: - if purge: - log.debug("Purging user %s", user_to_update) - self.service.user_manager.purge(user_to_update) - else: - self.service.user_manager.delete(user_to_update) + user = service.user_manager.create(email=email, username=username, password=password) + else: + raise exceptions.NotImplemented() + item = user.to_dict(view="element", value_mapper={"id": trans.security.encode_id, "total_disk_usage": float}) + return item + + +@router.get( + "/api/users", + name="get_users", + description="Return a collection of users. Filters will only work if enabled in config or user is admin.", + response_model_exclude_unset=True, +) +def index( + trans: ProvidesUserContext = DependsOnTrans, + deleted: bool = UsersDeletedQueryParam, + f_email: Optional[str] = FilterEmailQueryParam, + f_name: Optional[str] = FilterNameQueryParam, + f_any: Optional[str] = FilterAnyQueryParam, + service: UsersService = depends(UsersService), +) -> List[Union[UserModel, LimitedUserModel]]: + return service.get_index(trans=trans, deleted=deleted, f_email=f_email, f_name=f_name, f_any=f_any) + + +@router.get( + "/api/users/{user_id}", + name="get_user", + summary="Return information about a specified or the current user. Only admin can see deleted or other users", +) +def show( + trans: ProvidesHistoryContext = DependsOnTrans, + user_id: FlexibleUserIdType = FlexibleUserIdPathParam, + deleted: Optional[bool] = UserDeletedQueryParam, + service: UsersService = depends(UsersService), +) -> AnyUserModel: + user_deleted = deleted or False + return service.show_user(trans=trans, user_id=user_id, deleted=user_deleted) + + +@router.put( + "/api/users/{user_id}", name="update_user", summary="Update the values of a user. Only admin can update others." +) +def update( + trans: ProvidesUserContext = DependsOnTrans, + user_id: FlexibleUserIdType = FlexibleUserIdPathParam, + payload: Dict[Any, Any] = UserUpdateBody, + deleted: Optional[bool] = UserDeletedQueryParam, + service: UsersService = depends(UsersService), +) -> DetailedUserModel: + deleted = deleted or False + current_user = trans.user + user_to_update = service.get_non_anonymous_user_full(trans, user_id, deleted=deleted) + service.user_deserializer.deserialize(user_to_update, payload, user=current_user, trans=trans) + return service.user_to_detailed_model(user_to_update) + + +@router.delete( + "/api/users/{user_id}", + name="delete_user", + summary="Delete a user. Only admins can delete others or purge users.", +) +def delete( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + payload: Optional[UserDeletionPayload] = UserDeletionBody, + service: UsersService = depends(UsersService), +) -> DetailedUserModel: + user_to_update = service.user_manager.by_id(user_id) + if payload: + purge = payload.purge + else: + purge = False + if trans.user_is_admin: + if purge: + log.debug("Purging user %s", user_to_update) + service.user_manager.purge(user_to_update) else: - if trans.user == user_to_update: - self.service.user_manager.delete(user_to_update) - else: - raise exceptions.InsufficientPermissionsException("You may only delete your own account.") - return self.service.user_to_detailed_model(user_to_update) - - @router.post( - "/api/users/{user_id}/send_activation_email", - name="send_activation_email", - summary="Sends activation email to user.", - require_admin=True, - ) - def send_activation_email( - self, - trans: ProvidesUserContext = DependsOnTrans, - user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, - ): - user = trans.sa_session.query(trans.model.User).get(user_id) - if not user: - raise exceptions.ObjectNotFound("User not found for given id.") - if not self.service.user_manager.send_activation_email(trans, user.email, user.username): - raise exceptions.MessageException("Unable to send activation email.") + service.user_manager.delete(user_to_update) + else: + if trans.user == user_to_update: + service.user_manager.delete(user_to_update) + else: + raise exceptions.InsufficientPermissionsException("You may only delete your own account.") + return service.user_to_detailed_model(user_to_update) + + +@router.post( + "/api/users/{user_id}/send_activation_email", + name="send_activation_email", + summary="Sends activation email to user.", + require_admin=True, +) +def send_activation_email( + trans: ProvidesUserContext = DependsOnTrans, + user_id: DecodedDatabaseIdField = UserIdPathParamQueryParam, + service: UsersService = depends(UsersService), +): + user = trans.sa_session.query(trans.model.User).get(user_id) + if not user: + raise exceptions.ObjectNotFound("User not found for given id.") + if not service.user_manager.send_activation_email(trans, user.email, user.username): + raise exceptions.MessageException("Unable to send activation email.") class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController, UsesFormDefinitionsMixin): diff --git a/lib/galaxy/webapps/galaxy/api/visualizations.py b/lib/galaxy/webapps/galaxy/api/visualizations.py index 6fdba09f3358..8fce4bca19e5 100644 --- a/lib/galaxy/webapps/galaxy/api/visualizations.py +++ b/lib/galaxy/webapps/galaxy/api/visualizations.py @@ -109,132 +109,135 @@ ) -@router.cbv -class FastAPIVisualizations: - service: VisualizationsService = depends(VisualizationsService) - - @router.get( - "/api/visualizations", - summary="Returns visualizations for the current user.", +@router.get( + "/api/visualizations", + summary="Returns visualizations for the current user.", +) +async def index( + response: Response, + trans: ProvidesUserContext = DependsOnTrans, + deleted: bool = DeletedQueryParam, + limit: Optional[int] = LimitQueryParam, + offset: Optional[int] = OffsetQueryParam, + user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, + show_own: bool = ShowOwnQueryParam, + show_published: bool = ShowPublishedQueryParam, + show_shared: bool = ShowSharedQueryParam, + sort_by: VisualizationSortByEnum = SortByQueryParam, + sort_desc: bool = SortDescQueryParam, + search: Optional[str] = SearchQueryParam, + service: VisualizationsService = depends(VisualizationsService), +) -> VisualizationSummaryList: + payload = VisualizationIndexQueryPayload.construct( + deleted=deleted, + user_id=user_id, + show_published=show_published, + show_own=show_own, + show_shared=show_shared, + sort_by=sort_by, + sort_desc=sort_desc, + limit=limit, + offset=offset, + search=search, ) - async def index( - self, - response: Response, - trans: ProvidesUserContext = DependsOnTrans, - deleted: bool = DeletedQueryParam, - limit: Optional[int] = LimitQueryParam, - offset: Optional[int] = OffsetQueryParam, - user_id: Optional[DecodedDatabaseIdField] = UserIdQueryParam, - show_own: bool = ShowOwnQueryParam, - show_published: bool = ShowPublishedQueryParam, - show_shared: bool = ShowSharedQueryParam, - sort_by: VisualizationSortByEnum = SortByQueryParam, - sort_desc: bool = SortDescQueryParam, - search: Optional[str] = SearchQueryParam, - ) -> VisualizationSummaryList: - payload = VisualizationIndexQueryPayload.construct( - deleted=deleted, - user_id=user_id, - show_published=show_published, - show_own=show_own, - show_shared=show_shared, - sort_by=sort_by, - sort_desc=sort_desc, - limit=limit, - offset=offset, - search=search, - ) - entries, total_matches = self.service.index(trans, payload, include_total_count=True) - response.headers["total_matches"] = str(total_matches) - return entries + entries, total_matches = service.index(trans, payload, include_total_count=True) + response.headers["total_matches"] = str(total_matches) + return entries - @router.get( - "/api/visualizations/{id}/sharing", - summary="Get the current sharing status of the given Visualization.", - ) - def sharing( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - ) -> SharingStatus: - """Return the sharing status of the item.""" - return self.service.shareable_service.sharing(trans, id) - - @router.put( - "/api/visualizations/{id}/enable_link_access", - summary="Makes this item accessible by a URL link.", - ) - def enable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - ) -> SharingStatus: - """Makes this item accessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.enable_link_access(trans, id) - - @router.put( - "/api/visualizations/{id}/disable_link_access", - summary="Makes this item inaccessible by a URL link.", - ) - def disable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - ) -> SharingStatus: - """Makes this item inaccessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.disable_link_access(trans, id) - - @router.put( - "/api/visualizations/{id}/publish", - summary="Makes this item public and accessible by a URL link.", - ) - def publish( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - ) -> SharingStatus: - """Makes this item publicly available by a URL link and return the current sharing status.""" - return self.service.shareable_service.publish(trans, id) - - @router.put( - "/api/visualizations/{id}/unpublish", - summary="Removes this item from the published list.", - ) - def unpublish( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - ) -> SharingStatus: - """Removes this item from the published list and return the current sharing status.""" - return self.service.shareable_service.unpublish(trans, id) - - @router.put( - "/api/visualizations/{id}/share_with_users", - summary="Share this item with specific users.", - ) - def share_with_users( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - payload: ShareWithPayload = Body(...), - ) -> ShareWithStatus: - """Shares this item with specific users and return the current sharing status.""" - return self.service.shareable_service.share_with_users(trans, id, payload) - - @router.put( - "/api/visualizations/{id}/slug", - summary="Set a new slug for this shared item.", - status_code=status.HTTP_204_NO_CONTENT, - ) - def set_slug( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = VisualizationIdPathParam, - payload: SetSlugPayload = Body(...), - ): - """Sets a new slug to access this item by URL. The new slug must be unique.""" - self.service.shareable_service.set_slug(trans, id, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) + +@router.get( + "/api/visualizations/{id}/sharing", + summary="Get the current sharing status of the given Visualization.", +) +def sharing( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + service: VisualizationsService = depends(VisualizationsService), +) -> SharingStatus: + """Return the sharing status of the item.""" + return service.shareable_service.sharing(trans, id) + + +@router.put( + "/api/visualizations/{id}/enable_link_access", + summary="Makes this item accessible by a URL link.", +) +def enable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + service: VisualizationsService = depends(VisualizationsService), +) -> SharingStatus: + """Makes this item accessible by a URL link and return the current sharing status.""" + return service.shareable_service.enable_link_access(trans, id) + + +@router.put( + "/api/visualizations/{id}/disable_link_access", + summary="Makes this item inaccessible by a URL link.", +) +def disable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + service: VisualizationsService = depends(VisualizationsService), +) -> SharingStatus: + """Makes this item inaccessible by a URL link and return the current sharing status.""" + return service.shareable_service.disable_link_access(trans, id) + + +@router.put( + "/api/visualizations/{id}/publish", + summary="Makes this item public and accessible by a URL link.", +) +def publish( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + service: VisualizationsService = depends(VisualizationsService), +) -> SharingStatus: + """Makes this item publicly available by a URL link and return the current sharing status.""" + return service.shareable_service.publish(trans, id) + + +@router.put( + "/api/visualizations/{id}/unpublish", + summary="Removes this item from the published list.", +) +def unpublish( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + service: VisualizationsService = depends(VisualizationsService), +) -> SharingStatus: + """Removes this item from the published list and return the current sharing status.""" + return service.shareable_service.unpublish(trans, id) + + +@router.put( + "/api/visualizations/{id}/share_with_users", + summary="Share this item with specific users.", +) +def share_with_users( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + payload: ShareWithPayload = Body(...), + service: VisualizationsService = depends(VisualizationsService), +) -> ShareWithStatus: + """Shares this item with specific users and return the current sharing status.""" + return service.shareable_service.share_with_users(trans, id, payload) + + +@router.put( + "/api/visualizations/{id}/slug", + summary="Set a new slug for this shared item.", + status_code=status.HTTP_204_NO_CONTENT, +) +def set_slug( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = VisualizationIdPathParam, + payload: SetSlugPayload = Body(...), + service: VisualizationsService = depends(VisualizationsService), +): + """Sets a new slug to access this item by URL. The new slug must be unique.""" + service.shareable_service.set_slug(trans, id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) class VisualizationsController(BaseGalaxyAPIController, UsesVisualizationMixin, UsesAnnotations): diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 086ddc2f40af..75ab309c7add 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -1198,317 +1198,327 @@ def __encode_invocation(self, invocation, **kwd): ) -@router.cbv -class FastAPIWorkflows: - service: WorkflowsService = depends(WorkflowsService) - - @router.get( - "/api/workflows", - summary="Lists stored workflows viewable by the user.", - response_description="A list with summary stored workflow information per viewable entry.", +@router.get( + "/api/workflows", + summary="Lists stored workflows viewable by the user.", + response_description="A list with summary stored workflow information per viewable entry.", +) +def index( + response: Response, + trans: ProvidesUserContext = DependsOnTrans, + show_deleted: bool = DeletedQueryParam, + show_hidden: bool = HiddenQueryParam, + missing_tools: bool = MissingToolsQueryParam, + show_published: Optional[bool] = ShowPublishedQueryParam, + show_shared: Optional[bool] = ShowSharedQueryParam, + sort_by: Optional[WorkflowSortByEnum] = SortByQueryParam, + sort_desc: Optional[bool] = SortDescQueryParam, + limit: Optional[int] = LimitQueryParam, + offset: Optional[int] = OffsetQueryParam, + search: Optional[str] = SearchQueryParam, + skip_step_counts: bool = SkipStepCountsQueryParam, + service: WorkflowsService = depends(WorkflowsService), +) -> List[Dict[str, Any]]: + """Lists stored workflows viewable by the user.""" + payload = WorkflowIndexPayload.construct( + show_published=show_published, + show_hidden=show_hidden, + show_deleted=show_deleted, + show_shared=show_shared, + missing_tools=missing_tools, + sort_by=sort_by, + sort_desc=sort_desc, + limit=limit, + offset=offset, + search=search, + skip_step_counts=skip_step_counts, ) - def index( - self, - response: Response, - trans: ProvidesUserContext = DependsOnTrans, - show_deleted: bool = DeletedQueryParam, - show_hidden: bool = HiddenQueryParam, - missing_tools: bool = MissingToolsQueryParam, - show_published: Optional[bool] = ShowPublishedQueryParam, - show_shared: Optional[bool] = ShowSharedQueryParam, - sort_by: Optional[WorkflowSortByEnum] = SortByQueryParam, - sort_desc: Optional[bool] = SortDescQueryParam, - limit: Optional[int] = LimitQueryParam, - offset: Optional[int] = OffsetQueryParam, - search: Optional[str] = SearchQueryParam, - skip_step_counts: bool = SkipStepCountsQueryParam, - ) -> List[Dict[str, Any]]: - """Lists stored workflows viewable by the user.""" - payload = WorkflowIndexPayload.construct( - show_published=show_published, - show_hidden=show_hidden, - show_deleted=show_deleted, - show_shared=show_shared, - missing_tools=missing_tools, - sort_by=sort_by, - sort_desc=sort_desc, - limit=limit, - offset=offset, - search=search, - skip_step_counts=skip_step_counts, - ) - workflows, total_matches = self.service.index(trans, payload, include_total_count=True) - response.headers["total_matches"] = str(total_matches) - return workflows + workflows, total_matches = service.index(trans, payload, include_total_count=True) + response.headers["total_matches"] = str(total_matches) + return workflows - @router.get( - "/api/workflows/{id}/sharing", - summary="Get the current sharing status of the given item.", - ) - def sharing( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ) -> SharingStatus: - """Return the sharing status of the item.""" - return self.service.shareable_service.sharing(trans, id) - - @router.put( - "/api/workflows/{id}/enable_link_access", - summary="Makes this item accessible by a URL link.", - ) - def enable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ) -> SharingStatus: - """Makes this item accessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.enable_link_access(trans, id) - - @router.put( - "/api/workflows/{id}/disable_link_access", - summary="Makes this item inaccessible by a URL link.", - ) - def disable_link_access( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ) -> SharingStatus: - """Makes this item inaccessible by a URL link and return the current sharing status.""" - return self.service.shareable_service.disable_link_access(trans, id) - - @router.put( - "/api/workflows/{id}/publish", - summary="Makes this item public and accessible by a URL link.", - ) - def publish( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ) -> SharingStatus: - """Makes this item publicly available by a URL link and return the current sharing status.""" - return self.service.shareable_service.publish(trans, id) - - @router.put( - "/api/workflows/{id}/unpublish", - summary="Removes this item from the published list.", - ) - def unpublish( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ) -> SharingStatus: - """Removes this item from the published list and return the current sharing status.""" - return self.service.shareable_service.unpublish(trans, id) - - @router.put( - "/api/workflows/{id}/share_with_users", - summary="Share this item with specific users.", + +@router.get( + "/api/workflows/{id}/sharing", + summary="Get the current sharing status of the given item.", +) +def sharing( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +) -> SharingStatus: + """Return the sharing status of the item.""" + return service.shareable_service.sharing(trans, id) + + +@router.put( + "/api/workflows/{id}/enable_link_access", + summary="Makes this item accessible by a URL link.", +) +def enable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +) -> SharingStatus: + """Makes this item accessible by a URL link and return the current sharing status.""" + return service.shareable_service.enable_link_access(trans, id) + + +@router.put( + "/api/workflows/{id}/disable_link_access", + summary="Makes this item inaccessible by a URL link.", +) +def disable_link_access( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +) -> SharingStatus: + """Makes this item inaccessible by a URL link and return the current sharing status.""" + return service.shareable_service.disable_link_access(trans, id) + + +@router.put( + "/api/workflows/{id}/publish", + summary="Makes this item public and accessible by a URL link.", +) +def publish( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +) -> SharingStatus: + """Makes this item publicly available by a URL link and return the current sharing status.""" + return service.shareable_service.publish(trans, id) + + +@router.put( + "/api/workflows/{id}/unpublish", + summary="Removes this item from the published list.", +) +def unpublish( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +) -> SharingStatus: + """Removes this item from the published list and return the current sharing status.""" + return service.shareable_service.unpublish(trans, id) + + +@router.put( + "/api/workflows/{id}/share_with_users", + summary="Share this item with specific users.", +) +def share_with_users( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + payload: ShareWithPayload = Body(...), + service: WorkflowsService = depends(WorkflowsService), +) -> ShareWithStatus: + """Shares this item with specific users and return the current sharing status.""" + return service.shareable_service.share_with_users(trans, id, payload) + + +@router.put( + "/api/workflows/{id}/slug", + summary="Set a new slug for this shared item.", + status_code=status.HTTP_204_NO_CONTENT, +) +def set_slug( + trans: ProvidesUserContext = DependsOnTrans, + id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + payload: SetSlugPayload = Body(...), + service: WorkflowsService = depends(WorkflowsService), +): + """Sets a new slug to access this item by URL. The new slug must be unique.""" + service.shareable_service.set_slug(trans, id, payload) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.delete( + "/api/workflows/{workflow_id}", + summary="Add the deleted flag to a workflow.", +) +def delete_workflow( + trans: ProvidesUserContext = DependsOnTrans, + workflow_id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +): + service.delete(trans, workflow_id) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post( + "/api/workflows/{workflow_id}/undelete", + summary="Remove the deleted flag from a workflow.", +) +def undelete_workflow( + trans: ProvidesUserContext = DependsOnTrans, + workflow_id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + service: WorkflowsService = depends(WorkflowsService), +): + service.undelete(trans, workflow_id) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/api/workflows/{workflow_id}/versions", + summary="List all versions of a workflow.", +) +def show_versions( + trans: ProvidesUserContext = DependsOnTrans, + workflow_id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, + instance: Optional[bool] = InstanceQueryParam, + service: WorkflowsService = depends(WorkflowsService), +): + return service.get_versions(trans, workflow_id, instance) + + +@router.get( + "/api/workflows/menu", + summary="Get workflows present in the tools panel.", +) +def get_workflow_menu( + trans: ProvidesUserContext = DependsOnTrans, + show_deleted: Optional[bool] = DeletedQueryParam, + show_hidden: Optional[bool] = HiddenQueryParam, + missing_tools: Optional[bool] = MissingToolsQueryParam, + show_published: Optional[bool] = ShowPublishedQueryParam, + show_shared: Optional[bool] = ShowSharedQueryParam, + service: WorkflowsService = depends(WorkflowsService), +): + payload = WorkflowIndexPayload( + show_published=show_published, + show_hidden=show_hidden, + show_deleted=show_deleted, + show_shared=show_shared, + missing_tools=missing_tools, ) - def share_with_users( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - payload: ShareWithPayload = Body(...), - ) -> ShareWithStatus: - """Shares this item with specific users and return the current sharing status.""" - return self.service.shareable_service.share_with_users(trans, id, payload) - - @router.put( - "/api/workflows/{id}/slug", - summary="Set a new slug for this shared item.", - status_code=status.HTTP_204_NO_CONTENT, + return service.get_workflow_menu( + trans, + payload=payload, ) - def set_slug( - self, - trans: ProvidesUserContext = DependsOnTrans, - id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - payload: SetSlugPayload = Body(...), - ): - """Sets a new slug to access this item by URL. The new slug must be unique.""" - self.service.shareable_service.set_slug(trans, id, payload) - return Response(status_code=status.HTTP_204_NO_CONTENT) - @router.delete( - "/api/workflows/{workflow_id}", - summary="Add the deleted flag to a workflow.", - ) - def delete_workflow( - self, - trans: ProvidesUserContext = DependsOnTrans, - workflow_id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ): - self.service.delete(trans, workflow_id) - return Response(status_code=status.HTTP_204_NO_CONTENT) - @router.post( - "/api/workflows/{workflow_id}/undelete", - summary="Remove the deleted flag from a workflow.", +@router.post( + "/api/invocations/{invocation_id}/prepare_store_download", + summary="Prepare a workflow invocation export-style download.", +) +def prepare_store_download( + trans: ProvidesUserContext = DependsOnTrans, + invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, + payload: PrepareStoreDownloadPayload = Body(...), + invocations_service: InvocationsService = depends(InvocationsService), +) -> AsyncFile: + return invocations_service.prepare_store_download( + trans, + invocation_id, + payload, ) - def undelete_workflow( - self, - trans: ProvidesUserContext = DependsOnTrans, - workflow_id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - ): - self.service.undelete(trans, workflow_id) - return Response(status_code=status.HTTP_204_NO_CONTENT) - @router.get( - "/api/workflows/{workflow_id}/versions", - summary="List all versions of a workflow.", - ) - def show_versions( - self, - trans: ProvidesUserContext = DependsOnTrans, - workflow_id: DecodedDatabaseIdField = StoredWorkflowIDPathParam, - instance: Optional[bool] = InstanceQueryParam, - ): - return self.service.get_versions(trans, workflow_id, instance) - @router.get( - "/api/workflows/menu", - summary="Get workflows present in the tools panel.", +@router.post( + "/api/invocations/{invocation_id}/write_store", + summary="Prepare a workflow invocation export-style download and write to supplied URI.", +) +def write_store( + trans: ProvidesUserContext = DependsOnTrans, + invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, + payload: WriteInvocationStoreToPayload = Body(...), + invocations_service: InvocationsService = depends(InvocationsService), +) -> AsyncTaskResultSummary: + rval = invocations_service.write_store( + trans, + invocation_id, + payload, ) - def get_workflow_menu( - self, - trans: ProvidesUserContext = DependsOnTrans, - show_deleted: Optional[bool] = DeletedQueryParam, - show_hidden: Optional[bool] = HiddenQueryParam, - missing_tools: Optional[bool] = MissingToolsQueryParam, - show_published: Optional[bool] = ShowPublishedQueryParam, - show_shared: Optional[bool] = ShowSharedQueryParam, - ): - payload = WorkflowIndexPayload( - show_published=show_published, - show_hidden=show_hidden, - show_deleted=show_deleted, - show_shared=show_shared, - missing_tools=missing_tools, - ) - return self.service.get_workflow_menu( - trans, - payload=payload, - ) + return rval -@router.cbv -class FastAPIInvocations: - invocations_service: InvocationsService = depends(InvocationsService) +# TODO: remove this endpoint after 23.1 release +@router.get( + "/api/invocations/{invocation_id}/biocompute", + summary="Return a BioCompute Object for the workflow invocation.", + deprecated=True, +) +def export_invocation_bco( + trans: ProvidesUserContext = DependsOnTrans, + invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, + merge_history_metadata: Optional[bool] = Query(default=False), + invocations_service: InvocationsService = depends(InvocationsService), +): + """ + The BioCompute Object endpoints are in beta - important details such + as how inputs and outputs are represented, how the workflow is encoded, + and how author and version information is encoded, and how URLs are + generated will very likely change in important ways over time. + + **Deprecation Notice**: please use the asynchronous short_term_storage export system instead. + + 1. call POST `api/invocations/{id}/prepare_store_download` with payload: + ``` + { + model_store_format: bco.json + } + ``` + 2. Get `storageRequestId` from response and poll GET `api/short_term_storage/${storageRequestId}/ready` until `SUCCESS` - @router.post( - "/api/invocations/{invocation_id}/prepare_store_download", - summary="Prepare a workflow invocation export-style download.", - ) - def prepare_store_download( - self, - trans: ProvidesUserContext = DependsOnTrans, - invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, - payload: PrepareStoreDownloadPayload = Body(...), - ) -> AsyncFile: - return self.invocations_service.prepare_store_download( - trans, - invocation_id, - payload, - ) + 3. Get the resulting file with `api/short_term_storage/${storageRequestId}` + """ + bco = _deprecated_generate_bco(trans, invocation_id, merge_history_metadata, invocations_service) + return json.loads(bco) - @router.post( - "/api/invocations/{invocation_id}/write_store", - summary="Prepare a workflow invocation export-style download and write to supplied URI.", - ) - def write_store( - self, - trans: ProvidesUserContext = DependsOnTrans, - invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, - payload: WriteInvocationStoreToPayload = Body(...), - ) -> AsyncTaskResultSummary: - rval = self.invocations_service.write_store( - trans, - invocation_id, - payload, - ) - return rval - # TODO: remove this endpoint after 23.1 release - @router.get( - "/api/invocations/{invocation_id}/biocompute", - summary="Return a BioCompute Object for the workflow invocation.", - deprecated=True, - ) - def export_invocation_bco( - self, - trans: ProvidesUserContext = DependsOnTrans, - invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, - merge_history_metadata: Optional[bool] = Query(default=False), - ): - """ - The BioCompute Object endpoints are in beta - important details such - as how inputs and outputs are represented, how the workflow is encoded, - and how author and version information is encoded, and how URLs are - generated will very likely change in important ways over time. - - **Deprecation Notice**: please use the asynchronous short_term_storage export system instead. - - 1. call POST `api/invocations/{id}/prepare_store_download` with payload: - ``` - { - model_store_format: bco.json - } - ``` - 2. Get `storageRequestId` from response and poll GET `api/short_term_storage/${storageRequestId}/ready` until `SUCCESS` - - 3. Get the resulting file with `api/short_term_storage/${storageRequestId}` - """ - bco = self._deprecated_generate_bco(trans, invocation_id, merge_history_metadata) - return json.loads(bco) - - # TODO: remove this endpoint after 23.1 release - @router.get( - "/api/invocations/{invocation_id}/biocompute/download", - summary="Return a BioCompute Object for the workflow invocation as a file for download.", - response_class=StreamingResponse, - deprecated=True, +# TODO: remove this endpoint after 23.1 release +@router.get( + "/api/invocations/{invocation_id}/biocompute/download", + summary="Return a BioCompute Object for the workflow invocation as a file for download.", + response_class=StreamingResponse, + deprecated=True, +) +def download_invocation_bco( + trans: ProvidesUserContext = DependsOnTrans, + invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, + merge_history_metadata: Optional[bool] = Query(default=False), + invocations_service: InvocationsService = depends(InvocationsService), +): + """ + The BioCompute Object endpoints are in beta - important details such + as how inputs and outputs are represented, how the workflow is encoded, + and how author and version information is encoded, and how URLs are + generated will very likely change in important ways over time. + + **Deprecation Notice**: please use the asynchronous short_term_storage export system instead. + + 1. call POST `api/invocations/{id}/prepare_store_download` with payload: + ``` + { + model_store_format: bco.json + } + ``` + 2. Get `storageRequestId` from response and poll GET `api/short_term_storage/${storageRequestId}/ready` until `SUCCESS` + + 3. Get the resulting file with `api/short_term_storage/${storageRequestId}` + """ + bco = _deprecated_generate_bco(trans, invocation_id, merge_history_metadata, invocations_service) + return StreamingResponse( + content=BytesIO(bco), + media_type="application/json", + headers={ + "Content-Disposition": f'attachment; filename="bco_{trans.security.encode_id(invocation_id)}.json"', + "Access-Control-Expose-Headers": "Content-Disposition", + }, ) - def download_invocation_bco( - self, - trans: ProvidesUserContext = DependsOnTrans, - invocation_id: DecodedDatabaseIdField = InvocationIDPathParam, - merge_history_metadata: Optional[bool] = Query(default=False), - ): - """ - The BioCompute Object endpoints are in beta - important details such - as how inputs and outputs are represented, how the workflow is encoded, - and how author and version information is encoded, and how URLs are - generated will very likely change in important ways over time. - - **Deprecation Notice**: please use the asynchronous short_term_storage export system instead. - - 1. call POST `api/invocations/{id}/prepare_store_download` with payload: - ``` - { - model_store_format: bco.json - } - ``` - 2. Get `storageRequestId` from response and poll GET `api/short_term_storage/${storageRequestId}/ready` until `SUCCESS` - - 3. Get the resulting file with `api/short_term_storage/${storageRequestId}` - """ - bco = self._deprecated_generate_bco(trans, invocation_id, merge_history_metadata) - return StreamingResponse( - content=BytesIO(bco), - media_type="application/json", - headers={ - "Content-Disposition": f'attachment; filename="bco_{trans.security.encode_id(invocation_id)}.json"', - "Access-Control-Expose-Headers": "Content-Disposition", - }, - ) - # TODO: remove this after 23.1 release - def _deprecated_generate_bco( - self, trans, invocation_id: DecodedDatabaseIdField, merge_history_metadata: Optional[bool] - ): - export_options = BcoExportOptions( - galaxy_url=trans.request.url_path, - galaxy_version=VERSION, - merge_history_metadata=merge_history_metadata or False, - ) - return self.invocations_service.deprecated_generate_invocation_bco(trans, invocation_id, export_options) + +# TODO: remove this after 23.1 release +def _deprecated_generate_bco( + trans, + invocation_id: DecodedDatabaseIdField, + merge_history_metadata: Optional[bool], + invocations_service: InvocationsService, +): + export_options = BcoExportOptions( + galaxy_url=trans.request.url_path, + galaxy_version=VERSION, + merge_history_metadata=merge_history_metadata or False, + ) + return invocations_service.deprecated_generate_invocation_bco(trans, invocation_id, export_options) diff --git a/lib/tool_shed/webapp/api2/authenticate.py b/lib/tool_shed/webapp/api2/authenticate.py index 7e4ea1c4ee2d..68635f766630 100644 --- a/lib/tool_shed/webapp/api2/authenticate.py +++ b/lib/tool_shed/webapp/api2/authenticate.py @@ -12,16 +12,14 @@ router = Router(tags=["authenticate"]) -@router.cbv -class FastAPIAuthenticate: - authentication_service: AuthenticationService = depends(AuthenticationService) - - @router.get( - "/api/authenticate/baseauth", - summary="Returns returns an API key for authenticated user based on BaseAuth headers.", - operation_id="authenticate__baseauth", - ) - def get_api_key(self, request: Request) -> APIKeyResponse: - authorization = request.headers.get("Authorization") - auth = {"HTTP_AUTHORIZATION": authorization} - return self.authentication_service.get_api_key(auth, request) +@router.get( + "/api/authenticate/baseauth", + summary="Returns returns an API key for authenticated user based on BaseAuth headers.", + operation_id="authenticate__baseauth", +) +def get_api_key( + request: Request, authentication_service: AuthenticationService = depends(AuthenticationService) +) -> APIKeyResponse: + authorization = request.headers.get("Authorization") + auth = {"HTTP_AUTHORIZATION": authorization} + return authentication_service.get_api_key(auth, request) diff --git a/lib/tool_shed/webapp/api2/categories.py b/lib/tool_shed/webapp/api2/categories.py index cd8696f65cfb..311e1a99f448 100644 --- a/lib/tool_shed/webapp/api2/categories.py +++ b/lib/tool_shed/webapp/api2/categories.py @@ -27,66 +27,72 @@ router = Router(tags=["categories"]) -@router.cbv -class FastAPICategories: - category_manager: CategoryManager = depends(CategoryManager) +@router.post( + "/api/categories", + description="create a category", + operation_id="categories__create", + require_admin=True, +) +def create( + trans: SessionRequestContext = DependsOnTrans, + request: CreateCategoryRequest = Body(...), + category_manager: CategoryManager = depends(CategoryManager), +) -> CategoryResponse: + category = category_manager.create(trans, request) + return category_manager.to_model(category) - @router.post( - "/api/categories", - description="create a category", - operation_id="categories__create", - require_admin=True, - ) - def create( - self, trans: SessionRequestContext = DependsOnTrans, request: CreateCategoryRequest = Body(...) - ) -> CategoryResponse: - category = self.category_manager.create(trans, request) - return self.category_manager.to_model(category) - @router.get( - "/api/categories", - description="index category", - operation_id="categories__index", - ) - def index(self, trans: SessionRequestContext = DependsOnTrans) -> List[CategoryResponse]: - """ - Return a list of dictionaries that contain information about each Category. - """ - deleted = False - categories = self.category_manager.index_db(trans, deleted) - return [self.category_manager.to_model(c) for c in categories] +@router.get( + "/api/categories", + description="index category", + operation_id="categories__index", +) +def index( + trans: SessionRequestContext = DependsOnTrans, + category_manager: CategoryManager = depends(CategoryManager), +) -> List[CategoryResponse]: + """ + Return a list of dictionaries that contain information about each Category. + """ + deleted = False + categories = category_manager.index_db(trans, deleted) + return [category_manager.to_model(c) for c in categories] - @router.get( - "/api/categories/{encoded_category_id}", - description="show category", - operation_id="categories__show", - ) - def show(self, encoded_category_id: str = CategoryIdPathParam) -> CategoryResponse: - """ - Return a list of dictionaries that contain information about each Category. - """ - category = self.category_manager.get(encoded_category_id) - return self.category_manager.to_model(category) - @router.get( - "/api/categories/{encoded_category_id}/repositories", - description="display repositories by category", - operation_id="categories__repositories", +@router.get( + "/api/categories/{encoded_category_id}", + description="show category", + operation_id="categories__show", +) +def show( + encoded_category_id: str = CategoryIdPathParam, + category_manager: CategoryManager = depends(CategoryManager), +) -> CategoryResponse: + """ + Return a list of dictionaries that contain information about each Category. + """ + category = category_manager.get(encoded_category_id) + return category_manager.to_model(category) + + +@router.get( + "/api/categories/{encoded_category_id}/repositories", + description="display repositories by category", + operation_id="categories__repositories", +) +def repositories( + trans: SessionRequestContext = DependsOnTrans, + encoded_category_id: str = CategoryIdPathParam, + installable: bool = CategoryRepositoriesInstallableQueryParam, + sort_key: str = CategoryRepositoriesSortKeyQueryParam, + sort_order: str = CategoryRepositoriesSortOrderQueryParam, + page: Optional[int] = CategoryRepositoriesPageQueryParam, +) -> RepositoriesByCategory: + return repositories_by_category( + trans.app, + encoded_category_id, + page=page, + sort_key=sort_key, + sort_order=sort_order, + installable=installable, ) - def repositories( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_category_id: str = CategoryIdPathParam, - installable: bool = CategoryRepositoriesInstallableQueryParam, - sort_key: str = CategoryRepositoriesSortKeyQueryParam, - sort_order: str = CategoryRepositoriesSortOrderQueryParam, - page: Optional[int] = CategoryRepositoriesPageQueryParam, - ) -> RepositoriesByCategory: - return repositories_by_category( - trans.app, - encoded_category_id, - page=page, - sort_key=sort_key, - sort_order=sort_order, - installable=installable, - ) diff --git a/lib/tool_shed/webapp/api2/configuration.py b/lib/tool_shed/webapp/api2/configuration.py index 815039150e81..f8a6447e30af 100644 --- a/lib/tool_shed/webapp/api2/configuration.py +++ b/lib/tool_shed/webapp/api2/configuration.py @@ -8,17 +8,13 @@ router = Router(tags=["configuration"]) -@router.cbv -class FastAPIConfiguration: - app: ToolShedApp = depends(ToolShedApp) - - @router.get( - "/api/version", - operation_id="configuration__version", +@router.get( + "/api/version", + operation_id="configuration__version", +) +def version(app: ToolShedApp = depends(ToolShedApp)) -> Version: + return Version( + version_major=app.config.version_major, + version=app.config.version, + api_version="v2", ) - def version(self) -> Version: - return Version( - version_major=self.app.config.version_major, - version=self.app.config.version, - api_version="v2", - ) diff --git a/lib/tool_shed/webapp/api2/repositories.py b/lib/tool_shed/webapp/api2/repositories.py index 4c809e2c1cec..b432ecf8c13c 100644 --- a/lib/tool_shed/webapp/api2/repositories.py +++ b/lib/tool_shed/webapp/api2/repositories.py @@ -29,12 +29,12 @@ check_updates, create_repository, get_install_info, - get_ordered_installable_revisions, + get_ordered_installable_revisions as _get_ordered_installable_revisions, get_repository_metadata_dict, get_repository_metadata_for_management, index_repositories, readmes, - reset_metadata_on_repository, + reset_metadata_on_repository as _reset_metadata_on_repository, search, to_detailed_model, to_model, @@ -95,417 +95,425 @@ class RepositoryUpdateRequestFormData(RepositoryUpdateRequest): pass -@router.cbv -class FastAPIRepositories: - app: ToolShedApp = depends(ToolShedApp) - - @router.get( - "/api/repositories", - description="Get a list of repositories or perform a search.", - operation_id="repositories__index", - ) - def index( - self, - q: Optional[str] = RepositoryIndexQueryParam, - page: Optional[int] = RepositorySearchPageQueryParam, - page_size: Optional[int] = RepositorySearchPageSizeQueryParam, - deleted: Optional[bool] = RepositoryIndexDeletedQueryParam, - owner: Optional[str] = RepositoryIndexOwnerQueryParam, - name: Optional[str] = RepositoryIndexNameQueryParam, - trans: SessionRequestContext = DependsOnTrans, - ) -> IndexResponse: - if q: - assert page is not None - assert page_size is not None - search_results = search(trans, q, page, page_size) - return RepositorySearchResults(**search_results) - # See API notes - was added in https://github.com/galaxyproject/galaxy/pull/3626/files - # but I think is currently unused. So probably we should just drop it until someone - # complains. - # elif params.tool_ids: - # response = index_tool_ids(self.app, params.tool_ids) - # return response - else: - repositories = index_repositories(self.app, name, owner, deleted or False) - return [to_model(self.app, r) for r in repositories] - - @router.get( - "/api/repositories/get_repository_revision_install_info", - description="Get information used by the install client to install this repository.", - operation_id="repositories__legacy_install_info", +@router.get( + "/api/repositories", + description="Get a list of repositories or perform a search.", + operation_id="repositories__index", +) +def index( + q: Optional[str] = RepositoryIndexQueryParam, + page: Optional[int] = RepositorySearchPageQueryParam, + page_size: Optional[int] = RepositorySearchPageSizeQueryParam, + deleted: Optional[bool] = RepositoryIndexDeletedQueryParam, + owner: Optional[str] = RepositoryIndexOwnerQueryParam, + name: Optional[str] = RepositoryIndexNameQueryParam, + trans: SessionRequestContext = DependsOnTrans, + app: ToolShedApp = depends(ToolShedApp), +) -> IndexResponse: + if q: + assert page is not None + assert page_size is not None + search_results = search(trans, q, page, page_size) + return RepositorySearchResults(**search_results) + # See API notes - was added in https://github.com/galaxyproject/galaxy/pull/3626/files + # but I think is currently unused. So probably we should just drop it until someone + # complains. + # elif params.tool_ids: + # response = index_tool_ids(app, params.tool_ids) + # return response + else: + repositories = index_repositories(app, name, owner, deleted or False) + return [to_model(app, r) for r in repositories] + + +@router.get( + "/api/repositories/get_repository_revision_install_info", + description="Get information used by the install client to install this repository.", + operation_id="repositories__legacy_install_info", +) +def legacy_install_info( + trans: SessionRequestContext = DependsOnTrans, + name: str = RequiredRepoNameParam, + owner: str = RequiredRepoOwnerParam, + changeset_revision: str = RequiredChangesetParam, +) -> list: + legacy_install_info = get_install_info( + trans, + name, + owner, + changeset_revision, ) - def legacy_install_info( - self, - trans: SessionRequestContext = DependsOnTrans, - name: str = RequiredRepoNameParam, - owner: str = RequiredRepoOwnerParam, - changeset_revision: str = RequiredChangesetParam, - ) -> list: - legacy_install_info = get_install_info( - trans, - name, - owner, - changeset_revision, - ) - return list(legacy_install_info) + return list(legacy_install_info) - @router.get( - "/api/repositories/install_info", - description="Get information used by the install client to install this repository.", - operation_id="repositories__install_info", - ) - def install_info( - self, - trans: SessionRequestContext = DependsOnTrans, - name: str = RequiredRepoNameParam, - owner: str = RequiredRepoOwnerParam, - changeset_revision: str = RequiredChangesetParam, - ) -> InstallInfo: - # A less problematic version of the above API, but I guess we - # need to maintain the older version for older Galaxy API clients - # for... sometime... or forever. - legacy_install_info = get_install_info( - trans, - name, - owner, - changeset_revision, - ) - return from_legacy_install_info(legacy_install_info) - - @router.get( - "/api/repositories/{encoded_repository_id}/metadata", - description="Get information about repository metadata", - operation_id="repositories__metadata", - # See comment below. - # response_model=RepositoryMetadata, - ) - def metadata( - self, - encoded_repository_id: str = RepositoryIdPathParam, - downloadable_only: bool = DownloadableQueryParam, - ) -> dict: - recursive = True - as_dict = get_repository_metadata_dict(self.app, encoded_repository_id, recursive, downloadable_only) - # fails 1020 if we try to use the model - I guess repository dependencies - # are getting lost - return as_dict - # return _hack_fastapi_4428(as_dict) - - @router.get( - "/api_internal/repositories/{encoded_repository_id}/metadata", - description="Get information about repository metadata", - operation_id="repositories__internal_metadata", - response_model=RepositoryMetadata, - ) - def metadata_internal( - self, - encoded_repository_id: str = RepositoryIdPathParam, - downloadable_only: bool = DownloadableQueryParam, - ) -> dict: - recursive = True - as_dict = get_repository_metadata_dict(self.app, encoded_repository_id, recursive, downloadable_only) - return _hack_fastapi_4428(as_dict) - - @router.get( - "/api/repositories/get_ordered_installable_revisions", - description="Get an ordered list of the repository changeset revisions that are installable", - operation_id="repositories__get_ordered_installable_revisions", - ) - def get_ordered_installable_revisions( - self, - owner: Optional[str] = OptionalRepositoryOwnerParam, - name: Optional[str] = OptionalRepositoryNameParam, - tsr_id: Optional[str] = OptionalRepositoryIdParam, - ) -> List[str]: - return get_ordered_installable_revisions(self.app, name, owner, tsr_id) - - @router.post( - "/api/repositories/reset_metadata_on_repository", - description="reset metadata on a repository", - operation_id="repositories__reset_legacy", - ) - def reset_metadata_on_repository_legacy( - self, - trans: SessionRequestContext = DependsOnTrans, - request: ResetMetadataOnRepositoryRequest = depend_on_either_json_or_form_data( - ResetMetadataOnRepositoryRequest - ), - ) -> ResetMetadataOnRepositoryResponse: - return reset_metadata_on_repository(trans, request.repository_id) - - @router.post( - "/api/repositories/{encoded_repository_id}/reset_metadata", - description="reset metadata on a repository", - operation_id="repositories__reset", - ) - def reset_metadata_on_repository( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - ) -> ResetMetadataOnRepositoryResponse: - return reset_metadata_on_repository(trans, encoded_repository_id) - - @router.get( - "/api/repositories/updates", - operation_id="repositories__update", - ) - @router.get( - "/api/repositories/updates/", - ) - def updates( - self, - owner: Optional[str] = OptionalRepositoryOwnerParam, - name: Optional[str] = OptionalRepositoryNameParam, - changeset_revision: str = RequiredRepositoryChangesetRevisionParam, - hexlify: Optional[bool] = OptionalHexlifyParam, - ): - request = UpdatesRequest( - name=name, - owner=owner, - changeset_revision=changeset_revision, - hexlify=hexlify, - ) - return Response(content=check_updates(self.app, request)) - @router.post( - "/api/repositories", - description="create a new repository", - operation_id="repositories__create", +@router.get( + "/api/repositories/install_info", + description="Get information used by the install client to install this repository.", + operation_id="repositories__install_info", +) +def install_info( + trans: SessionRequestContext = DependsOnTrans, + name: str = RequiredRepoNameParam, + owner: str = RequiredRepoOwnerParam, + changeset_revision: str = RequiredChangesetParam, +) -> InstallInfo: + # A less problematic version of the above API, but I guess we + # need to maintain the older version for older Galaxy API clients + # for... sometime... or forever. + legacy_install_info = get_install_info( + trans, + name, + owner, + changeset_revision, ) - def create( - self, - trans: SessionRequestContext = DependsOnTrans, - request: CreateRepositoryRequest = Body(...), - ) -> Repository: - db_repository = create_repository( - trans, - request, - ) - return to_model(self.app, db_repository) + return from_legacy_install_info(legacy_install_info) - @router.get( - "/api/repositories/{encoded_repository_id}", - operation_id="repositories__show", - ) - def show( - self, - encoded_repository_id: str = RepositoryIdPathParam, - ) -> DetailedRepository: - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - return to_detailed_model(self.app, repository) - - @router.get( - "/api/repositories/{encoded_repository_id}/permissions", - operation_id="repositories__permissions", - ) - def permissions( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - ) -> RepositoryPermissions: - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_update_repo(trans, repository): - raise InsufficientPermissionsException( - "You do not have permission to inspect repository repository permissions." - ) - return RepositoryPermissions( - allow_push=trans.app.security_agent.usernames_that_can_push(repository), - can_manage=can_manage_repo(trans, repository), - can_push=can_update_repo(trans, repository), - ) - @router.get( - "/api/repositories/{encoded_repository_id}/allow_push", - operation_id="repositories__show_allow_push", - ) - def show_allow_push( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - ) -> List[str]: - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") - return trans.app.security_agent.usernames_that_can_push(repository) +@router.get( + "/api/repositories/{encoded_repository_id}/metadata", + description="Get information about repository metadata", + operation_id="repositories__metadata", + # See comment below. + # response_model=RepositoryMetadata, +) +def metadata( + encoded_repository_id: str = RepositoryIdPathParam, + downloadable_only: bool = DownloadableQueryParam, + app: ToolShedApp = depends(ToolShedApp), +) -> dict: + recursive = True + as_dict = get_repository_metadata_dict(app, encoded_repository_id, recursive, downloadable_only) + # fails 1020 if we try to use the model - I guess repository dependencies + # are getting lost + return as_dict + # return _hack_fastapi_4428(as_dict) + + +@router.get( + "/api_internal/repositories/{encoded_repository_id}/metadata", + description="Get information about repository metadata", + operation_id="repositories__internal_metadata", + response_model=RepositoryMetadata, +) +def metadata_internal( + encoded_repository_id: str = RepositoryIdPathParam, + downloadable_only: bool = DownloadableQueryParam, + app: ToolShedApp = depends(ToolShedApp), +) -> dict: + recursive = True + as_dict = get_repository_metadata_dict(app, encoded_repository_id, recursive, downloadable_only) + return _hack_fastapi_4428(as_dict) + + +@router.get( + "/api/repositories/get_ordered_installable_revisions", + description="Get an ordered list of the repository changeset revisions that are installable", + operation_id="repositories__get_ordered_installable_revisions", +) +def get_ordered_installable_revisions( + owner: Optional[str] = OptionalRepositoryOwnerParam, + name: Optional[str] = OptionalRepositoryNameParam, + tsr_id: Optional[str] = OptionalRepositoryIdParam, + app: ToolShedApp = depends(ToolShedApp), +) -> List[str]: + return _get_ordered_installable_revisions(app, name, owner, tsr_id) + + +@router.post( + "/api/repositories/reset_metadata_on_repository", + description="reset metadata on a repository", + operation_id="repositories__reset_legacy", +) +def reset_metadata_on_repository_legacy( + trans: SessionRequestContext = DependsOnTrans, + request: ResetMetadataOnRepositoryRequest = depend_on_either_json_or_form_data(ResetMetadataOnRepositoryRequest), +) -> ResetMetadataOnRepositoryResponse: + return _reset_metadata_on_repository(trans, request.repository_id) - @router.post( - "/api/repositories/{encoded_repository_id}/allow_push/{username}", - operation_id="repositories__add_allow_push", - ) - def add_allow_push( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - username: str = UsernameIdPathParam, - ) -> List[str]: - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") - repository.set_allow_push([username]) - return trans.app.security_agent.usernames_that_can_push(repository) - @router.put( - "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious", - operation_id="repositories__set_malicious", - status_code=status.HTTP_204_NO_CONTENT, - ) - def set_malicious( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - changeset_revision: str = ChangesetRevisionPathParam, - ): - repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision) - repository_metadata.malicious = True - trans.sa_session.add(repository_metadata) - with transaction(trans.sa_session): - trans.sa_session.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.delete( - "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious", - operation_id="repositories__unset_malicious", - status_code=status.HTTP_204_NO_CONTENT, - ) - def unset_malicious( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - changeset_revision: str = ChangesetRevisionPathParam, - ): - repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision) - repository_metadata.malicious = False - trans.sa_session.add(repository_metadata) - with transaction(trans.sa_session): - trans.sa_session.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.put( - "/api/repositories/{encoded_repository_id}/deprecated", - operation_id="repositories__set_deprecated", - status_code=status.HTTP_204_NO_CONTENT, +@router.post( + "/api/repositories/{encoded_repository_id}/reset_metadata", + description="reset metadata on a repository", + operation_id="repositories__reset", +) +def reset_metadata_on_repository( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, +) -> ResetMetadataOnRepositoryResponse: + return _reset_metadata_on_repository(trans, encoded_repository_id) + + +@router.get( + "/api/repositories/updates", + operation_id="repositories__update", +) +@router.get( + "/api/repositories/updates/", +) +def updates( + owner: Optional[str] = OptionalRepositoryOwnerParam, + name: Optional[str] = OptionalRepositoryNameParam, + changeset_revision: str = RequiredRepositoryChangesetRevisionParam, + hexlify: Optional[bool] = OptionalHexlifyParam, + app: ToolShedApp = depends(ToolShedApp), +): + request = UpdatesRequest( + name=name, + owner=owner, + changeset_revision=changeset_revision, + hexlify=hexlify, ) - def set_deprecated( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - ): - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") - repository.deprecated = True - trans.sa_session.add(repository) - with transaction(trans.sa_session): - trans.sa_session.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.delete( - "/api/repositories/{encoded_repository_id}/deprecated", - operation_id="repositories__unset_deprecated", - status_code=status.HTTP_204_NO_CONTENT, + return Response(content=check_updates(app, request)) + + +@router.post( + "/api/repositories", + description="create a new repository", + operation_id="repositories__create", +) +def create( + trans: SessionRequestContext = DependsOnTrans, + request: CreateRepositoryRequest = Body(...), + app: ToolShedApp = depends(ToolShedApp), +) -> Repository: + db_repository = create_repository( + trans, + request, ) - def unset_deprecated( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - ): - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") - repository.deprecated = False - trans.sa_session.add(repository) - with transaction(trans.sa_session): - trans.sa_session.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.delete( - "/api/repositories/{encoded_repository_id}/allow_push/{username}", - operation_id="repositories__remove_allow_push", + return to_model(app, db_repository) + + +@router.get( + "/api/repositories/{encoded_repository_id}", + operation_id="repositories__show", +) +def show( + encoded_repository_id: str = RepositoryIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +) -> DetailedRepository: + repository = get_repository_in_tool_shed(app, encoded_repository_id) + return to_detailed_model(app, repository) + + +@router.get( + "/api/repositories/{encoded_repository_id}/permissions", + operation_id="repositories__permissions", +) +def permissions( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +) -> RepositoryPermissions: + repository = get_repository_in_tool_shed(app, encoded_repository_id) + if not can_update_repo(trans, repository): + raise InsufficientPermissionsException( + "You do not have permission to inspect repository repository permissions." + ) + return RepositoryPermissions( + allow_push=trans.app.security_agent.usernames_that_can_push(repository), + can_manage=can_manage_repo(trans, repository), + can_push=can_update_repo(trans, repository), ) - def remove_allow_push( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_repository_id: str = RepositoryIdPathParam, - username: str = UsernameIdPathParam, - ) -> List[str]: - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - if not can_manage_repo(trans, repository): + + +@router.get( + "/api/repositories/{encoded_repository_id}/allow_push", + operation_id="repositories__show_allow_push", +) +def show_allow_push( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +) -> List[str]: + repository = get_repository_in_tool_shed(app, encoded_repository_id) + if not can_manage_repo(trans, repository): + raise InsufficientPermissionsException("You do not have permission to update this repository.") + return trans.app.security_agent.usernames_that_can_push(repository) + + +@router.post( + "/api/repositories/{encoded_repository_id}/allow_push/{username}", + operation_id="repositories__add_allow_push", +) +def add_allow_push( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + username: str = UsernameIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +) -> List[str]: + repository = get_repository_in_tool_shed(app, encoded_repository_id) + if not can_manage_repo(trans, repository): + raise InsufficientPermissionsException("You do not have permission to update this repository.") + repository.set_allow_push([username]) + return trans.app.security_agent.usernames_that_can_push(repository) + + +@router.put( + "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious", + operation_id="repositories__set_malicious", + status_code=status.HTTP_204_NO_CONTENT, +) +def set_malicious( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + changeset_revision: str = ChangesetRevisionPathParam, +): + repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision) + repository_metadata.malicious = True + trans.sa_session.add(repository_metadata) + with transaction(trans.sa_session): + trans.sa_session.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.delete( + "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious", + operation_id="repositories__unset_malicious", + status_code=status.HTTP_204_NO_CONTENT, +) +def unset_malicious( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + changeset_revision: str = ChangesetRevisionPathParam, +): + repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision) + repository_metadata.malicious = False + trans.sa_session.add(repository_metadata) + with transaction(trans.sa_session): + trans.sa_session.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.put( + "/api/repositories/{encoded_repository_id}/deprecated", + operation_id="repositories__set_deprecated", + status_code=status.HTTP_204_NO_CONTENT, +) +def set_deprecated( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +): + repository = get_repository_in_tool_shed(app, encoded_repository_id) + if not can_manage_repo(trans, repository): + raise InsufficientPermissionsException("You do not have permission to update this repository.") + repository.deprecated = True + trans.sa_session.add(repository) + with transaction(trans.sa_session): + trans.sa_session.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.delete( + "/api/repositories/{encoded_repository_id}/deprecated", + operation_id="repositories__unset_deprecated", + status_code=status.HTTP_204_NO_CONTENT, +) +def unset_deprecated( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +): + repository = get_repository_in_tool_shed(app, encoded_repository_id) + if not can_manage_repo(trans, repository): + raise InsufficientPermissionsException("You do not have permission to update this repository.") + repository.deprecated = False + trans.sa_session.add(repository) + with transaction(trans.sa_session): + trans.sa_session.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.delete( + "/api/repositories/{encoded_repository_id}/allow_push/{username}", + operation_id="repositories__remove_allow_push", +) +def remove_allow_push( + trans: SessionRequestContext = DependsOnTrans, + encoded_repository_id: str = RepositoryIdPathParam, + username: str = UsernameIdPathParam, + app: ToolShedApp = depends(ToolShedApp), +) -> List[str]: + repository = get_repository_in_tool_shed(app, encoded_repository_id) + if not can_manage_repo(trans, repository): + raise InsufficientPermissionsException("You do not have permission to update this repository.") + repository.set_allow_push(None, remove_auth=username) + return trans.app.security_agent.usernames_that_can_push(repository) + + +@router.post( + "/api/repositories/{encoded_repository_id}/changeset_revision", + description="upload new revision to the repository", + operation_id="repositories__create_revision", +) +async def create_changeset_revision( + request: Request, + encoded_repository_id: str = RepositoryIdPathParam, + commit_message: Optional[str] = CommitMessageQueryParam, + trans: SessionRequestContext = DependsOnTrans, + files: Optional[List[UploadFile]] = None, + revision_request: RepositoryUpdateRequest = Depends(RepositoryUpdateRequestFormData.as_form), # type: ignore[attr-defined] + app: ToolShedApp = depends(ToolShedApp), +) -> RepositoryUpdate: + try: + # Code stolen from Marius' work in Galaxy's Tools API. + + files2: List[StarletteUploadFile] = cast(List[StarletteUploadFile], files or []) + # FastAPI's UploadFile is a very light wrapper around starlette's UploadFile + if not files2: + data = await request.form() + for value in data.values(): + if isinstance(value, StarletteUploadFile): + files2.append(value) + + repository = get_repository_in_tool_shed(app, encoded_repository_id) + + if not can_update_repo(trans, repository): raise InsufficientPermissionsException("You do not have permission to update this repository.") - repository.set_allow_push(None, remove_auth=username) - return trans.app.security_agent.usernames_that_can_push(repository) - @router.post( - "/api/repositories/{encoded_repository_id}/changeset_revision", - description="upload new revision to the repository", - operation_id="repositories__create_revision", - ) - async def create_changeset_revision( - self, - request: Request, - encoded_repository_id: str = RepositoryIdPathParam, - commit_message: Optional[str] = CommitMessageQueryParam, - trans: SessionRequestContext = DependsOnTrans, - files: Optional[List[UploadFile]] = None, - revision_request: RepositoryUpdateRequest = Depends(RepositoryUpdateRequestFormData.as_form), # type: ignore[attr-defined] - ) -> RepositoryUpdate: + assert trans.user + assert files2 + the_file = files2[0] + with tempfile.NamedTemporaryFile( + dir=trans.app.config.new_file_path, prefix="upload_file_data_", delete=False + ) as dest: + upload_file_like: IO[bytes] = the_file.file + shutil.copyfileobj(upload_file_like, dest) # type: ignore[misc] # https://github.com/python/mypy/issues/15031 + the_file.file.close() + filename = dest.name try: - # Code stolen from Marius' work in Galaxy's Tools API. - - files2: List[StarletteUploadFile] = cast(List[StarletteUploadFile], files or []) - # FastAPI's UploadFile is a very light wrapper around starlette's UploadFile - if not files2: - data = await request.form() - for value in data.values(): - if isinstance(value, StarletteUploadFile): - files2.append(value) - - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - - if not can_update_repo(trans, repository): - raise InsufficientPermissionsException("You do not have permission to update this repository.") - - assert trans.user - assert files2 - the_file = files2[0] - with tempfile.NamedTemporaryFile( - dir=trans.app.config.new_file_path, prefix="upload_file_data_", delete=False - ) as dest: - upload_file_like: IO[bytes] = the_file.file - shutil.copyfileobj(upload_file_like, dest) # type: ignore[misc] # https://github.com/python/mypy/issues/15031 - the_file.file.close() - filename = dest.name - try: - message = upload_tar_and_set_metadata( - trans, - trans.request.host, - repository, - filename, - commit_message or revision_request.commit_message or "Uploaded", - ) - return RepositoryUpdate(__root__=ValidRepostiroyUpdateMessage(message=message)) - finally: - if os.path.exists(filename): - os.remove(filename) - except Exception: - import logging - - log = logging.getLogger(__name__) - log.exception("Problem in here...") - raise - - @router.get( - "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/readmes", - description="fetch readmes for repository revision", - operation_id="repositories__readmes", - response_model=RepositoryRevisionReadmes, - ) - def get_readmes( - self, - encoded_repository_id: str = RepositoryIdPathParam, - changeset_revision: str = ChangesetRevisionPathParam, - ) -> dict: - repository = get_repository_in_tool_shed(self.app, encoded_repository_id) - return readmes(self.app, repository, changeset_revision) + message = upload_tar_and_set_metadata( + trans, + trans.request.host, + repository, + filename, + commit_message or revision_request.commit_message or "Uploaded", + ) + return RepositoryUpdate(__root__=ValidRepostiroyUpdateMessage(message=message)) + finally: + if os.path.exists(filename): + os.remove(filename) + except Exception: + import logging + + log = logging.getLogger(__name__) + log.exception("Problem in here...") + raise + + +@router.get( + "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/readmes", + description="fetch readmes for repository revision", + operation_id="repositories__readmes", + response_model=RepositoryRevisionReadmes, +) +def get_readmes( + encoded_repository_id: str = RepositoryIdPathParam, + changeset_revision: str = ChangesetRevisionPathParam, + app: ToolShedApp = depends(ToolShedApp), +) -> dict: + repository = get_repository_in_tool_shed(app, encoded_repository_id) + return readmes(app, repository, changeset_revision) def _hack_fastapi_4428(as_dict) -> dict: diff --git a/lib/tool_shed/webapp/api2/tools.py b/lib/tool_shed/webapp/api2/tools.py index 0d8c2f2d5524..55bde04c73ef 100644 --- a/lib/tool_shed/webapp/api2/tools.py +++ b/lib/tool_shed/webapp/api2/tools.py @@ -10,8 +10,8 @@ from tool_shed.managers.tools import search from tool_shed.managers.trs import ( get_tool, - service_info, - tool_classes, + service_info as _service_info, + tool_classes as _tool_classes, ) from tool_shed.structured_app import ToolShedApp from tool_shed.util.shed_index import build_index @@ -42,82 +42,79 @@ ) -@router.cbv -class FastAPITools: - app: ToolShedApp = depends(ToolShedApp) +@router.get( + "/api/tools", + operation_id="tools__index", +) +def index( + q: str = ToolsIndexQueryParam, + page: int = RepositorySearchPageQueryParam, + page_size: int = RepositorySearchPageSizeQueryParam, + trans: SessionRequestContext = DependsOnTrans, +): + search_results = search(trans, q, page, page_size) + return search_results - @router.get( - "/api/tools", - operation_id="tools__index", - ) - def index( - self, - q: str = ToolsIndexQueryParam, - page: int = RepositorySearchPageQueryParam, - page_size: int = RepositorySearchPageSizeQueryParam, - trans: SessionRequestContext = DependsOnTrans, - ): - search_results = search(trans, q, page, page_size) - return search_results - - @router.put( - "/api/tools/build_search_index", - operation_id="tools__build_search_index", - require_admin=True, - ) - def build_search_index(self) -> BuildSearchIndexResponse: - """Not part of the stable API, just something to simplify - bootstrapping tool sheds, scripting, testing, etc... - """ - config = self.app.config - repos_indexed, tools_indexed = build_index( - config.whoosh_index_dir, - config.file_path, - config.hgweb_config_dir, - config.database_connection, - ) - return BuildSearchIndexResponse( - repositories_indexed=repos_indexed, - tools_indexed=tools_indexed, - ) - - @router.get("/api/ga4gh/trs/v2/service-info", operation_id="tools_trs_service_info") - def service_info(self, request: Request) -> Service: - return service_info(self.app, request.url) - - @router.get("/api/ga4gh/trs/v2/toolClasses", operation_id="tools__trs_tool_classes") - def tool_classes(self) -> List[ToolClass]: - return tool_classes() - - @router.get( - "/api/ga4gh/trs/v2/tools", - operation_id="tools__trs_index", - ) - def trs_index( - self, - ): - # we probably want to be able to query the database at the - # tool level and such to do this right? - return [] - - @router.get( - "/api/ga4gh/trs/v2/tools/{tool_id}", - operation_id="tools__trs_get", + +@router.put( + "/api/tools/build_search_index", + operation_id="tools__build_search_index", + require_admin=True, +) +def build_search_index(app: ToolShedApp = depends(ToolShedApp)) -> BuildSearchIndexResponse: + """Not part of the stable API, just something to simplify + bootstrapping tool sheds, scripting, testing, etc... + """ + config = app.config + repos_indexed, tools_indexed = build_index( + config.whoosh_index_dir, + config.file_path, + config.hgweb_config_dir, + config.database_connection, ) - def trs_get( - self, - trans: SessionRequestContext = DependsOnTrans, - tool_id: str = TOOL_ID_PATH_PARAM, - ) -> Tool: - return get_tool(trans, tool_id) - - @router.get( - "/api/ga4gh/trs/v2/tools/{tool_id}/versions", - operation_id="tools__trs_get_versions", + return BuildSearchIndexResponse( + repositories_indexed=repos_indexed, + tools_indexed=tools_indexed, ) - def trs_get_versions( - self, - trans: SessionRequestContext = DependsOnTrans, - tool_id: str = TOOL_ID_PATH_PARAM, - ) -> List[ToolVersion]: - return get_tool(trans, tool_id).versions + + +@router.get("/api/ga4gh/trs/v2/service-info", operation_id="tools_trs_service_info") +def service_info(request: Request, app: ToolShedApp = depends(ToolShedApp)) -> Service: + return _service_info(app, request.url) + + +@router.get("/api/ga4gh/trs/v2/toolClasses", operation_id="tools__trs_tool_classes") +def tool_classes() -> List[ToolClass]: + return _tool_classes() + + +@router.get( + "/api/ga4gh/trs/v2/tools", + operation_id="tools__trs_index", +) +def trs_index(): + # we probably want to be able to query the database at the + # tool level and such to do this right? + return [] + + +@router.get( + "/api/ga4gh/trs/v2/tools/{tool_id}", + operation_id="tools__trs_get", +) +def trs_get( + trans: SessionRequestContext = DependsOnTrans, + tool_id: str = TOOL_ID_PATH_PARAM, +) -> Tool: + return get_tool(trans, tool_id) + + +@router.get( + "/api/ga4gh/trs/v2/tools/{tool_id}/versions", + operation_id="tools__trs_get_versions", +) +def trs_get_versions( + trans: SessionRequestContext = DependsOnTrans, + tool_id: str = TOOL_ID_PATH_PARAM, +) -> List[ToolVersion]: + return get_tool(trans, tool_id).versions diff --git a/lib/tool_shed/webapp/api2/users.py b/lib/tool_shed/webapp/api2/users.py index f55e51438cbc..034f3bc1a199 100644 --- a/lib/tool_shed/webapp/api2/users.py +++ b/lib/tool_shed/webapp/api2/users.py @@ -30,7 +30,7 @@ from tool_shed.managers.users import ( api_create_user, get_api_user, - index, + index as _index, ) from tool_shed.structured_app import ToolShedApp from tool_shed.webapp.model import ( @@ -98,194 +98,210 @@ class UiChangePasswordRequest(BaseModel): INVALID_LOGIN_OR_PASSWORD = "Invalid login or password" -@router.cbv -class FastAPIUsers: - app: ToolShedApp = depends(ToolShedApp) - user_manager: UserManager = depends(UserManager) - api_key_manager: ApiKeyManager = depends(ApiKeyManager) +@router.get( + "/api/users", + description="index users", + operation_id="users__index", +) +def index(trans: SessionRequestContext = DependsOnTrans) -> List[User]: + deleted = False + return _index(trans.app, deleted) - @router.get( - "/api/users", - description="index users", - operation_id="users__index", - ) - def index(self, trans: SessionRequestContext = DependsOnTrans) -> List[User]: - deleted = False - return index(trans.app, deleted) - - @router.post( - "/api/users", - description="create a user", - operation_id="users__create", - require_admin=True, - ) - def create(self, trans: SessionRequestContext = DependsOnTrans, request: CreateUserRequest = Body(...)) -> User: - return api_create_user(trans, request) - @router.get( - "/api/users/current", - description="show current user", - operation_id="users__current", - ) - def current(self, trans: SessionRequestContext = DependsOnTrans) -> User: - user = trans.user - if not user: - raise ObjectNotFound() +@router.post( + "/api/users", + description="create a user", + operation_id="users__create", + require_admin=True, +) +def create(trans: SessionRequestContext = DependsOnTrans, request: CreateUserRequest = Body(...)) -> User: + return api_create_user(trans, request) - return get_api_user(trans.app, user) - @router.get( - "/api/users/{encoded_user_id}", - description="show a user", - operation_id="users__show", - ) - def show(self, trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam) -> User: - user = suc.get_user(trans.app, encoded_user_id) - if user is None: - raise ObjectNotFound() - return get_api_user(trans.app, user) - - @router.get( - "/api/users/{encoded_user_id}/api_key", - name="get_or_create_api_key", - summary="Return the user's API key", - operation_id="users__get_or_create_api_key", - ) - def get_or_create_api_key( - self, trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam - ) -> str: - user = self._get_user(trans, encoded_user_id) - return self.api_key_manager.get_or_create_api_key(user) - - @router.post( - "/api/users/{encoded_user_id}/api_key", - summary="Creates a new API key for the user", - operation_id="users__create_api_key", - ) - def create_api_key( - self, trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam - ) -> str: - user = self._get_user(trans, encoded_user_id) - return self.api_key_manager.create_api_key(user).key - - @router.delete( - "/api/users/{encoded_user_id}/api_key", - summary="Delete the current API key of the user", - status_code=status.HTTP_204_NO_CONTENT, - operation_id="users__delete_api_key", - ) - def delete_api_key( - self, - trans: SessionRequestContext = DependsOnTrans, - encoded_user_id: str = UserIdPathParam, - ): - user = self._get_user(trans, encoded_user_id) - self.api_key_manager.delete_api_key(user) - return Response(status_code=status.HTTP_204_NO_CONTENT) - - def _get_user(self, trans: SessionRequestContext, encoded_user_id: str): - if encoded_user_id == "current": - user = trans.user - else: - user = suc.get_user(trans.app, encoded_user_id) - if user is None: - raise ObjectNotFound() - if not (trans.user_is_admin or trans.user == user): - raise InsufficientPermissionsException() - return user - - @router.post( - "/api_internal/register", - description="register a user", - operation_id="users__internal_register", - ) - def register( - self, trans: SessionRequestContext = DependsOnTrans, request: UiRegisterRequest = Body(...) - ) -> UiRegisterResponse: - honeypot_field = request.bear_field - if honeypot_field != "": - message = "You've been flagged as a possible bot. If you are not, please try registering again and fill the form out carefully." - raise RequestParameterInvalidException(message) - - username = request.username - if username == "repos": - raise RequestParameterInvalidException("Cannot create a user with the username 'repos'") - self.user_manager.create(email=request.email, username=username, password=request.password) - if self.app.config.user_activation_on: - is_activation_sent = self.user_manager.send_activation_email(trans, request.email, username) - if is_activation_sent: - return UiRegisterResponse(email=request.email, activation_sent=True) - else: - return UiRegisterResponse( - email=request.email, - activation_sent=False, - activation_error=True, - contact_email=self.app.config.error_email_to, - ) +@router.get( + "/api/users/current", + description="show current user", + operation_id="users__current", +) +def current(trans: SessionRequestContext = DependsOnTrans) -> User: + user = trans.user + if not user: + raise ObjectNotFound() + + return get_api_user(trans.app, user) + + +@router.get( + "/api/users/{encoded_user_id}", + description="show a user", + operation_id="users__show", +) +def show(trans: SessionRequestContext = DependsOnTrans, encoded_user_id: str = UserIdPathParam) -> User: + user = suc.get_user(trans.app, encoded_user_id) + if user is None: + raise ObjectNotFound() + return get_api_user(trans.app, user) + + +@router.get( + "/api/users/{encoded_user_id}/api_key", + name="get_or_create_api_key", + summary="Return the user's API key", + operation_id="users__get_or_create_api_key", +) +def get_or_create_api_key( + trans: SessionRequestContext = DependsOnTrans, + encoded_user_id: str = UserIdPathParam, + api_key_manager: ApiKeyManager = depends(ApiKeyManager), +) -> str: + user = _get_user(trans, encoded_user_id) + return api_key_manager.get_or_create_api_key(user) + + +@router.post( + "/api/users/{encoded_user_id}/api_key", + summary="Creates a new API key for the user", + operation_id="users__create_api_key", +) +def create_api_key( + trans: SessionRequestContext = DependsOnTrans, + encoded_user_id: str = UserIdPathParam, + api_key_manager: ApiKeyManager = depends(ApiKeyManager), +) -> str: + user = _get_user(trans, encoded_user_id) + return api_key_manager.create_api_key(user).key + + +@router.delete( + "/api/users/{encoded_user_id}/api_key", + summary="Delete the current API key of the user", + status_code=status.HTTP_204_NO_CONTENT, + operation_id="users__delete_api_key", +) +def delete_api_key( + trans: SessionRequestContext = DependsOnTrans, + encoded_user_id: str = UserIdPathParam, + api_key_manager: ApiKeyManager = depends(ApiKeyManager), +): + user = _get_user(trans, encoded_user_id) + api_key_manager.delete_api_key(user) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post( + "/api_internal/register", + description="register a user", + operation_id="users__internal_register", +) +def register( + trans: SessionRequestContext = DependsOnTrans, + request: UiRegisterRequest = Body(...), + app: ToolShedApp = depends(ToolShedApp), + user_manager: UserManager = depends(UserManager), +) -> UiRegisterResponse: + honeypot_field = request.bear_field + if honeypot_field != "": + message = "You've been flagged as a possible bot. If you are not, please try registering again and fill the form out carefully." + raise RequestParameterInvalidException(message) + + username = request.username + if username == "repos": + raise RequestParameterInvalidException("Cannot create a user with the username 'repos'") + user_manager.create(email=request.email, username=username, password=request.password) + if app.config.user_activation_on: + is_activation_sent = user_manager.send_activation_email(trans, request.email, username) + if is_activation_sent: + return UiRegisterResponse(email=request.email, activation_sent=True) else: - return UiRegisterResponse(email=request.email) + return UiRegisterResponse( + email=request.email, + activation_sent=False, + activation_error=True, + contact_email=app.config.error_email_to, + ) + else: + return UiRegisterResponse(email=request.email) - @router.put( - "/api_internal/change_password", - description="reset a user", - operation_id="users__internal_change_password", - status_code=status.HTTP_204_NO_CONTENT, + +@router.put( + "/api_internal/change_password", + description="reset a user", + operation_id="users__internal_change_password", + status_code=status.HTTP_204_NO_CONTENT, +) +def change_password( + trans: SessionRequestContext = DependsOnTrans, + request: UiChangePasswordRequest = Body(...), + user_manager: UserManager = depends(UserManager), +): + password = request.password + current = request.current + if trans.user is None: + raise InsufficientPermissionsException("Must be logged into use this functionality") + user_id = trans.user.id + token = None + user, message = user_manager.change_password( + trans, password=password, current=current, token=token, confirm=password, id=user_id ) - def change_password( - self, trans: SessionRequestContext = DependsOnTrans, request: UiChangePasswordRequest = Body(...) - ): - password = request.password - current = request.current - if trans.user is None: - raise InsufficientPermissionsException("Must be logged into use this functionality") - user_id = trans.user.id - token = None - user, message = self.user_manager.change_password( - trans, password=password, current=current, token=token, confirm=password, id=user_id + if not user: + raise RequestParameterInvalidException(message) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.put( + "/api_internal/login", + description="login to web UI", + operation_id="users__internal_login", +) +def internal_login( + trans: SessionRequestContext = DependsOnTrans, + request: UiLoginRequest = Body(...), + user_manager: UserManager = depends(UserManager), +) -> UiLoginResponse: + log.info(f"top of internal_login {trans.session_csrf_token}") + ensure_csrf_token(trans, request) + login = request.login + password = request.password + user = user_manager.get_user_by_identity(login) + if user is None: + raise InsufficientPermissionsException(INVALID_LOGIN_OR_PASSWORD) + elif user.deleted: + message = ( + "This account has been marked deleted, contact your local Galaxy administrator to restore the account." ) - if not user: - raise RequestParameterInvalidException(message) - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.put( - "/api_internal/login", - description="login to web UI", - operation_id="users__internal_login", - ) - def internal_login( - self, trans: SessionRequestContext = DependsOnTrans, request: UiLoginRequest = Body(...) - ) -> UiLoginResponse: - log.info(f"top of internal_login {trans.session_csrf_token}") - ensure_csrf_token(trans, request) - login = request.login - password = request.password - user = self.user_manager.get_user_by_identity(login) - if user is None: - raise InsufficientPermissionsException(INVALID_LOGIN_OR_PASSWORD) - elif user.deleted: - message = ( - "This account has been marked deleted, contact your local Galaxy administrator to restore the account." - ) - if trans.app.config.error_email_to is not None: - message += f" Contact: {trans.app.config.error_email_to}." - raise InsufficientPermissionsException(message) - elif not trans.app.auth_manager.check_password(user, password, trans.request): - raise InsufficientPermissionsException(INVALID_LOGIN_OR_PASSWORD) - else: - handle_user_login(trans, user) - return UiLoginResponse() + if trans.app.config.error_email_to is not None: + message += f" Contact: {trans.app.config.error_email_to}." + raise InsufficientPermissionsException(message) + elif not trans.app.auth_manager.check_password(user, password, trans.request): + raise InsufficientPermissionsException(INVALID_LOGIN_OR_PASSWORD) + else: + handle_user_login(trans, user) + return UiLoginResponse() - @router.put( - "/api_internal/logout", - description="logout of web UI", - operation_id="users__internal_logout", - ) - def internal_logout( - self, trans: SessionRequestContext = DependsOnTrans, request: UiLogoutRequest = Body(...) - ) -> UiLogoutResponse: - ensure_csrf_token(trans, request) - handle_user_logout(trans, logout_all=request.logout_all) - return UiLogoutResponse() + +@router.put( + "/api_internal/logout", + description="logout of web UI", + operation_id="users__internal_logout", +) +def internal_logout( + trans: SessionRequestContext = DependsOnTrans, request: UiLogoutRequest = Body(...) +) -> UiLogoutResponse: + ensure_csrf_token(trans, request) + handle_user_logout(trans, logout_all=request.logout_all) + return UiLogoutResponse() + + +def _get_user(trans: SessionRequestContext, encoded_user_id: str): + if encoded_user_id == "current": + user = trans.user + else: + user = suc.get_user(trans.app, encoded_user_id) + if user is None: + raise ObjectNotFound() + if not (trans.user_is_admin or trans.user == user): + raise InsufficientPermissionsException() + return user def ensure_csrf_token(trans: SessionRequestContext, request: HasCsrfToken): diff --git a/packages/web_apps/setup.cfg b/packages/web_apps/setup.cfg index 4b5f5045e858..6faaa966b145 100644 --- a/packages/web_apps/setup.cfg +++ b/packages/web_apps/setup.cfg @@ -43,7 +43,6 @@ install_requires = Babel Cheetah3!=3.2.6.post2 fastapi>=0.71.0,!=0.89.0,<0.99 - fastapi-utils gunicorn gxformat2 importlib-resources;python_version<'3.9' diff --git a/pyproject.toml b/pyproject.toml index 625d43195168..fc89c6413819 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,6 @@ docutils = "!=0.17, !=0.17.1" dparse = "*" edam-ontology = "*" fastapi = ">=0.71.0, !=0.89.0, <0.99" # https://github.com/tiangolo/fastapi/issues/4041 https://github.com/tiangolo/fastapi/issues/5861 -fastapi-utils = "*" fs = "*" future = "*" galaxy_sequence_utils = "*"