Implement the GA4GH TRS API in the new tool shed.

This commit is contained in:
John Chilton
2023-09-26 13:46:59 -04:00
parent 24b3abeb3e
commit dd58bfcfe6
11 changed files with 681 additions and 2 deletions
@@ -342,6 +342,58 @@ mapping:
desc: |
Address to join mailing list
ga4gh_service_id:
type: str
required: false
desc: |
Service ID for GA4GH services (exposed via the service-info endpoint for the Galaxy DRS API).
If unset, one will be generated using the URL the target API requests are made against.
For more information on GA4GH service definitions - check out
https://github.com/ga4gh-discovery/ga4gh-service-registry
and https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
This value should likely reflect your service's URL. For instance for usegalaxy.org
this value should be org.usegalaxy. Particular Galaxy implementations will treat this
value as a prefix and append the service type to this ID. For instance for the DRS
service "id" (available via the DRS API) for the above configuration value would be
org.usegalaxy.drs.
ga4gh_service_organization_name:
type: str
required: false
desc: |
Service name for host organization (exposed via the service-info endpoint for the Galaxy DRS API).
If unset, one will be generated using ga4gh_service_id.
For more information on GA4GH service definitions - check out
https://github.com/ga4gh-discovery/ga4gh-service-registry
and https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
ga4gh_service_organization_url:
type: str
required: False
desc: |
Organization URL for host organization (exposed via the service-info endpoint for the Galaxy DRS API).
If unset, one will be generated using the URL the target API requests are made against.
For more information on GA4GH service definitions - check out
https://github.com/ga4gh-discovery/ga4gh-service-registry
and https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
ga4gh_service_environment:
type: str
required: False
desc: |
Service environment (exposed via the service-info endpoint for the Galaxy DRS API) for
implemented GA4GH services.
Suggested values are prod, test, dev, staging.
For more information on GA4GH service definitions - check out
https://github.com/ga4gh-discovery/ga4gh-service-registry
and https://editor.swagger.io/?url=https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-registry/develop/service-registry.yaml
use_heartbeat:
type: bool
default: true
+8 -1
View File
@@ -165,7 +165,14 @@ def get_error_response_for_request(request: Request, exc: MessageException) -> J
if "ga4gh" in path:
# When serving GA4GH APIs use limited exceptions to conform their expected
# error schema. Tailored to DRS currently.
content = {"status_code": status_code, "msg": error_dict["err_msg"]}
message = error_dict["err_msg"]
if "drs" in path:
content = {"status_code": status_code, "msg": message}
elif "trs" in path:
content = {"code": status_code, "message": message}
else:
# unknown schema - just yield the most useful error message
content = error_dict
else:
content = error_dict
+153
View File
@@ -0,0 +1,153 @@
from typing import (
Any,
cast,
Dict,
List,
Optional,
Tuple,
)
from starlette.datastructures import URL
from galaxy.exceptions import ObjectNotFound
from galaxy.util.tool_shed.common_util import remove_protocol_and_user_from_clone_url
from galaxy.version import VERSION
from tool_shed.context import ProvidesRepositoriesContext
from tool_shed.structured_app import ToolShedApp
from tool_shed.util.metadata_util import get_current_repository_metadata_for_changeset_revision
from tool_shed.webapp.model import (
Repository,
RepositoryMetadata,
)
from tool_shed_client.schema.trs import (
DescriptorType,
Tool,
ToolClass,
ToolVersion,
)
from tool_shed_client.schema.trs_service_info import (
Organization,
Service,
ServiceType,
)
from tool_shed_client.trs_util import decode_identifier
from .repositories import guid_to_repository
TRS_SERVICE_NAME = "Tool Shed TRS API"
TRS_SERVICE_DESCRIPTION = "Serves tool shed repository tools according to the GA4GH TRS specification"
def service_info(app: ToolShedApp, request_url: URL):
components = request_url.components
hostname = components.hostname
assert hostname
default_organization_id = ".".join(reversed(hostname.split(".")))
config = app.config
organization_id = cast(str, config.ga4gh_service_id or default_organization_id)
organization_name = cast(str, config.ga4gh_service_organization_name or organization_id)
organization_url = cast(str, config.ga4gh_service_organization_url or f"{components.scheme}://{components.netloc}")
organization = Organization(
url=organization_url,
name=organization_name,
)
service_type = ServiceType(
group="org.ga4gh",
artifact="trs",
version="2.1.0",
)
environment = config.ga4gh_service_environment
extra_kwds = {}
if environment:
extra_kwds["environment"] = environment
return Service(
id=organization_id + ".trs",
name=TRS_SERVICE_NAME,
description=TRS_SERVICE_DESCRIPTION,
organization=organization,
type=service_type,
version=VERSION,
**extra_kwds,
)
def tool_classes() -> List[ToolClass]:
return [ToolClass(id="galaxy_tool", name="Galaxy Tool", description="Galaxy XML Tools")]
def trs_tool_id_to_repository(trans: ProvidesRepositoriesContext, trs_tool_id: str) -> Repository:
guid = decode_identifier(trans.repositories_hostname, trs_tool_id)
guid = remove_protocol_and_user_from_clone_url(guid)
return guid_to_repository(trans.app, guid)
def get_repository_metadata_by_tool_version(
app: ToolShedApp, repository: Repository, tool_id: str
) -> Dict[str, RepositoryMetadata]:
versions = {}
for _, changeset in repository.installable_revisions(app):
metadata = get_current_repository_metadata_for_changeset_revision(app, repository, changeset)
tools: Optional[List[Dict[str, Any]]] = metadata.metadata.get("tools")
if not tools:
continue
for tool_metadata in tools:
if tool_metadata["id"] != tool_id:
continue
versions[tool_metadata["version"]] = metadata
return versions
def get_tools_for(repository_metadata: RepositoryMetadata) -> List[Dict[str, Any]]:
tools: Optional[List[Dict[str, Any]]] = repository_metadata.metadata.get("tools")
assert tools
return tools
def trs_tool_id_to_repository_metadata(
trans: ProvidesRepositoriesContext, trs_tool_id: str
) -> Optional[Tuple[Repository, Dict[str, RepositoryMetadata]]]:
tool_guid = decode_identifier(trans.repositories_hostname, trs_tool_id)
tool_guid = remove_protocol_and_user_from_clone_url(tool_guid)
_, tool_id = tool_guid.rsplit("/", 1)
repository = guid_to_repository(trans.app, tool_guid)
app = trans.app
versions: Dict[str, RepositoryMetadata] = get_repository_metadata_by_tool_version(app, repository, tool_id)
if not versions:
return None
return repository, versions
def get_tool(trans: ProvidesRepositoriesContext, trs_tool_id: str) -> Tool:
guid = decode_identifier(trans.repositories_hostname, trs_tool_id)
guid = remove_protocol_and_user_from_clone_url(guid)
repo_metadata = trs_tool_id_to_repository_metadata(trans, trs_tool_id)
if not repo_metadata:
raise ObjectNotFound()
repository, metadata_by_version = repo_metadata
repo_owner = repository.user.username
aliases: List[str] = [guid]
hostname = remove_protocol_and_user_from_clone_url(trans.repositories_hostname)
url = f"https://{hostname}/repos/{repo_owner}/{repository.name}"
versions: List[ToolVersion] = []
for tool_version_str, _ in metadata_by_version.items():
version_url = url # TODO:
tool_version = ToolVersion(
author=[repo_owner],
containerfile=False,
descriptor_type=[DescriptorType.GALAXY],
id=tool_version_str,
url=version_url,
verified=False,
)
versions.append(tool_version)
return Tool(
aliases=aliases,
id=trs_tool_id,
url=url,
toolclass=tool_classes()[0],
organization=repo_owner,
versions=versions,
)
@@ -1,4 +1,12 @@
from ..base.api import ShedApiTestCase
from tool_shed_client.schema.trs import (
Tool,
ToolClass,
)
from tool_shed_client.trs_util import encode_identifier
from ..base.api import (
ShedApiTestCase,
skip_if_api_v1,
)
class TestShedToolsApi(ShedApiTestCase):
@@ -32,3 +40,31 @@ class TestShedToolsApi(ShedApiTestCase):
# but if this tool has been installed a bunch by other tests - it might not be.
tool_search_hit = response.find_search_hit(repository)
assert tool_search_hit
@skip_if_api_v1
def test_trs_service_info(self):
service_info = self.api_interactor.get("ga4gh/trs/v2/service-info")
service_info.raise_for_status()
@skip_if_api_v1
def test_trs_tool_classes(self):
classes_response = self.api_interactor.get("ga4gh/trs/v2/toolClasses")
classes_response.raise_for_status()
classes = classes_response.json()
assert isinstance(classes, list)
assert len(classes) == 1
class0 = classes[0]
assert ToolClass(**class0)
@skip_if_api_v1
def test_trs_tool_list(self):
populator = self.populator
repository = populator.setup_column_maker_repo(prefix="toolstrsindex")
tool_id = populator.tool_guid(self, repository, "Add_a_column1")
tool_shed_base, encoded_tool_id = encode_identifier(tool_id)
print(encoded_tool_id)
url = f"ga4gh/trs/v2/tools/{encoded_tool_id}"
print(url)
tool_response = self.api_interactor.get(url)
tool_response.raise_for_status()
assert Tool(**tool_response.json())
+68
View File
@@ -1,8 +1,27 @@
import logging
from typing import List
from fastapi import (
Path,
Request,
)
from tool_shed.context import SessionRequestContext
from tool_shed.managers.tools import search
from tool_shed.managers.trs import (
get_tool,
service_info,
tool_classes,
)
from tool_shed.structured_app import ToolShedApp
from tool_shed.util.shed_index import build_index
from tool_shed_client.schema import BuildSearchIndexResponse
from tool_shed_client.schema.trs import (
Tool,
ToolClass,
ToolVersion,
)
from tool_shed_client.schema.trs_service_info import Service
from . import (
depends,
DependsOnTrans,
@@ -12,8 +31,16 @@ from . import (
ToolsIndexQueryParam,
)
log = logging.getLogger(__name__)
router = Router(tags=["tools"])
TOOL_ID_PATH_PARAM: str = Path(
...,
title="GA4GH TRS Tool ID",
description="See also https://ga4gh.github.io/tool-registry-service-schemas/DataModel/#trs-tool-and-trs-tool-version-ids",
)
@router.cbv
class FastAPITools:
@@ -53,3 +80,44 @@ class FastAPITools:
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",
)
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",
)
def trs_get_versions(
self,
trans: SessionRequestContext = DependsOnTrans,
tool_id: str = TOOL_ID_PATH_PARAM,
) -> List[ToolVersion]:
return get_tool(trans, tool_id).versions
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# must be run from a virtualenv with...
# https://github.com/koxudaxi/datamodel-code-generator
#for model in AccessMethod Checksum DrsObject Error AccessURL ContentsObject DrsService
#do
# datamodel-codegen --url "https://raw.githubusercontent.com/ga4gh/tool-registry-service-schemas/develop/openapi/ga4gh-tool-discovery.yaml" --output "$model.py"
#one
#datamodel-codegen --url "https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-info/v1.0.0/service-info.yaml#/components/schemas/Service" --output Service.py
datamodel-codegen --url "https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-info/v1.0.0/service-info.yaml#/paths/~1service-info" --output trs_service_info.py
datamodel-codegen --url "https://raw.githubusercontent.com/ga4gh/tool-registry-service-schemas/develop/openapi/openapi.yaml" --output trs.py
+216
View File
@@ -0,0 +1,216 @@
# generated by datamodel-codegen:
# filename: https://raw.githubusercontent.com/ga4gh/tool-registry-service-schemas/develop/openapi/openapi.yaml
# timestamp: 2022-12-20T21:01:58+00:00
from __future__ import annotations
from enum import Enum
from typing import (
Dict,
List,
Optional,
Union,
)
from pydantic import (
BaseModel,
Field,
)
class Checksum(BaseModel):
checksum: str = Field(..., description="The hex-string encoded checksum for the data. ")
type: str = Field(
...,
description="The digest method used to create the checksum.\nThe value (e.g. `sha-256`) SHOULD be listed as `Hash Name String` in the https://github.com/ga4gh-discovery/ga4gh-checksum/blob/master/hash-alg.csv[GA4GH Checksum Hash Algorithm Registry].\nOther values MAY be used, as long as implementors are aware of the issues discussed in https://tools.ietf.org/html/rfc6920#section-9.4[RFC6920].\nGA4GH may provide more explicit guidance for use of non-IANA-registered algorithms in the future.",
)
class FileType(Enum):
TEST_FILE = "TEST_FILE"
PRIMARY_DESCRIPTOR = "PRIMARY_DESCRIPTOR"
SECONDARY_DESCRIPTOR = "SECONDARY_DESCRIPTOR"
CONTAINERFILE = "CONTAINERFILE"
OTHER = "OTHER"
class ToolFile(BaseModel):
path: Optional[str] = Field(
None,
description="Relative path of the file. A descriptor's path can be used with the GA4GH .../{type}/descriptor/{relative_path} endpoint.",
)
file_type: Optional[FileType] = None
checksum: Optional[Checksum] = None
class ToolClass(BaseModel):
id: Optional[str] = Field(None, description="The unique identifier for the class.")
name: Optional[str] = Field(None, description="A short friendly name for the class.")
description: Optional[str] = Field(
None, description="A longer explanation of what this class is and what it can accomplish."
)
class ImageType(Enum):
Docker = "Docker"
Singularity = "Singularity"
Conda = "Conda"
class DescriptorType(Enum):
CWL = "CWL"
WDL = "WDL"
NFL = "NFL"
GALAXY = "GALAXY"
SMK = "SMK"
class DescriptorTypeVersion(BaseModel):
__root__: str = Field(
...,
description="The language version for a given descriptor type. The version should correspond to the actual declared version of the descriptor. For example, tools defined in CWL could have a version of `v1.0.2` whereas WDL tools may have a version of `1.0` or `draft-2`",
)
class DescriptorTypeWithPlain(Enum):
CWL = "CWL"
WDL = "WDL"
NFL = "NFL"
GALAXY = "GALAXY"
SMK = "SMK"
PLAIN_CWL = "PLAIN_CWL"
PLAIN_WDL = "PLAIN_WDL"
PLAIN_NFL = "PLAIN_NFL"
PLAIN_GALAXY = "PLAIN_GALAXY"
PLAIN_SMK = "PLAIN_SMK"
class FileWrapper(BaseModel):
content: Optional[str] = Field(
None, description="The content of the file itself. One of url or content is required."
)
checksum: Optional[List[Checksum]] = Field(
None,
description="A production (immutable) tool version is required to have a hashcode. Not required otherwise, but might be useful to detect changes. ",
example=[{"checksum": "ea2a5db69bd20a42976838790bc29294df3af02b", "type": "sha1"}],
)
image_type: Optional[Union[ImageType, DescriptorType]] = Field(
None, description="Optionally return additional information on the type of file this is"
)
url: Optional[str] = Field(
None,
description="Optional url to the underlying content, should include version information, and can include a git hash. Note that this URL should resolve to the raw unwrapped content that would otherwise be available in content. One of url or content is required.",
example={
"descriptorfile": {
"url": "https://raw.githubusercontent.com/ICGC-TCGA-PanCancer/pcawg_delly_workflow/ea2a5db69bd20a42976838790bc29294df3af02b/delly_docker/Delly.cwl"
},
"containerfile": {
"url": "https://raw.githubusercontent.com/ICGC-TCGA-PanCancer/pcawg_delly_workflow/c83478829802b4d36374870843821abe1b625a71/delly_docker/Dockerfile"
},
},
)
class Error(BaseModel):
code: int
message: Optional[str] = "Internal Server Error"
class ImageData(BaseModel):
registry_host: Optional[str] = Field(
None,
description="A docker registry or a URL to a Singularity registry. Used along with image_name to locate a specific image.",
example=["registry.hub.docker.com"],
)
image_name: Optional[str] = Field(
None,
description="Used in conjunction with a registry_url if provided to locate images.",
example=["quay.io/seqware/seqware_full/1.1", "ubuntu:latest"],
)
size: Optional[int] = Field(None, description="Size of the container in bytes.")
updated: Optional[str] = Field(None, description="Last time the container was updated.")
checksum: Optional[List[Checksum]] = Field(
None,
description="A production (immutable) tool version is required to have a hashcode. Not required otherwise, but might be useful to detect changes. This exposes the hashcode for specific image versions to verify that the container version pulled is actually the version that was indexed by the registry.",
example=[{"checksum": "77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182", "type": "sha256"}],
)
image_type: Optional[ImageType] = None
class ToolVersion(BaseModel):
author: Optional[List[str]] = Field(
None,
description="Contact information for the author of this version of the tool in the registry. (More complex authorship information is handled by the descriptor).",
)
name: Optional[str] = Field(None, description="The name of the version.")
url: str = Field(
...,
description="The URL for this tool version in this registry.",
example="http://agora.broadinstitute.org/tools/123456/versions/1",
)
id: str = Field(
..., description="An identifier of the version of this tool for this particular tool registry.", example="v1"
)
is_production: Optional[bool] = Field(
None,
description="This version of a tool is guaranteed to not change over time (for example, a tool built from a tag in git as opposed to a branch). A production quality tool is required to have a checksum",
)
images: Optional[List[ImageData]] = Field(
None,
description="All known docker images (and versions/hashes) used by this tool. If the tool has to evaluate any of the docker images strings at runtime, those ones cannot be reported here.",
)
descriptor_type: Optional[List[DescriptorType]] = Field(
None, description="The type (or types) of descriptors available."
)
descriptor_type_version: Optional[Dict[str, List[DescriptorTypeVersion]]] = Field(
None,
description="A map providing information about the language versions used in this tool. The keys should be the same values used in the `descriptor_type` field, and the value should be an array of all the language versions used for the given `descriptor_type`. Depending on the `descriptor_type` (e.g. CWL) multiple version values may be used in a single tool.",
example='{\n "WDL": ["1.0", "1.0"],\n "CWL": ["v1.0.2"],\n "NFL": ["DSL2"]\n}\n',
)
containerfile: Optional[bool] = Field(
None,
description="Reports if this tool has a containerfile available. (For Docker-based tools, this would indicate the presence of a Dockerfile)",
)
meta_version: Optional[str] = Field(
None,
description="The version of this tool version in the registry. Iterates when fields like the description, author, etc. are updated.",
)
verified: Optional[bool] = Field(
None, description="Reports whether this tool has been verified by a specific organization or individual."
)
verified_source: Optional[List[str]] = Field(
None, description="Source of metadata that can support a verified tool, such as an email or URL."
)
signed: Optional[bool] = Field(None, description="Reports whether this version of the tool has been signed.")
included_apps: Optional[List[str]] = Field(
None,
description="An array of IDs for the applications that are stored inside this tool.",
example=["https://bio.tools/tool/mytum.de/SNAP2/1", "https://bio.tools/bioexcel_seqqc"],
)
class Tool(BaseModel):
url: str = Field(
...,
description="The URL for this tool in this registry.",
example="http://agora.broadinstitute.org/tools/123456",
)
id: str = Field(..., description="A unique identifier of the tool, scoped to this registry.", example=123456)
aliases: Optional[List[str]] = Field(
None,
description="Support for this parameter is optional for tool registries that support aliases.\nA list of strings that can be used to identify this tool which could be straight up URLs. \nThis can be used to expose alternative ids (such as GUIDs) for a tool\nfor registries. Can be used to match tools across registries.",
)
organization: str = Field(..., description="The organization that published the image.")
name: Optional[str] = Field(None, description="The name of the tool.")
toolclass: ToolClass
description: Optional[str] = Field(None, description="The description of the tool.")
meta_version: Optional[str] = Field(
None,
description="The version of this tool in the registry. Iterates when fields like the description, author, etc. are updated.",
)
has_checker: Optional[bool] = Field(None, description="Whether this tool has a checker tool associated with it.")
checker_url: Optional[str] = Field(
None,
description="Optional url to the checker tool that will exit successfully if this tool produced the expected result given test data.",
)
versions: List[ToolVersion] = Field(..., description="A list of versions for this tool.")
@@ -0,0 +1,87 @@
# generated by datamodel-codegen:
# filename: https://raw.githubusercontent.com/ga4gh-discovery/ga4gh-service-info/v1.0.0/service-info.yaml#/paths/~1service-info
# timestamp: 2022-12-20T21:01:57+00:00
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import (
AnyUrl,
BaseModel,
Field,
)
class Organization(BaseModel):
name: str = Field(
..., description="Name of the organization responsible for the service", example="My organization"
)
url: AnyUrl = Field(
..., description="URL of the website of the organization (RFC 3986 format)", example="https://example.com"
)
class ServiceType(BaseModel):
group: str = Field(
...,
description="Namespace in reverse domain name format. Use `org.ga4gh` for implementations compliant with official GA4GH specifications. For services with custom APIs not standardized by GA4GH, or implementations diverging from official GA4GH specifications, use a different namespace (e.g. your organization's reverse domain name).",
example="org.ga4gh",
)
artifact: str = Field(
...,
description="Name of the API or GA4GH specification implemented. Official GA4GH types should be assigned as part of standards approval process. Custom artifacts are supported.",
example="beacon",
)
version: str = Field(
...,
description="Version of the API or specification. GA4GH specifications use semantic versioning.",
example="1.0.0",
)
class Service(BaseModel):
id: str = Field(
...,
description="Unique ID of this service. Reverse domain name notation is recommended, though not required. The identifier should attempt to be globally unique so it can be used in downstream aggregator services e.g. Service Registry.",
example="org.ga4gh.myservice",
)
name: str = Field(..., description="Name of this service. Should be human readable.", example="My project")
type: ServiceType
description: Optional[str] = Field(
None,
description="Description of the service. Should be human readable and provide information about the service.",
example="This service provides...",
)
organization: Organization = Field(..., description="Organization providing the service")
contactUrl: Optional[AnyUrl] = Field(
None,
description="URL of the contact for the provider of this service, e.g. a link to a contact form (RFC 3986 format), or an email (RFC 2368 format).",
example="mailto:support@example.com",
)
documentationUrl: Optional[AnyUrl] = Field(
None,
description="URL of the documentation of this service (RFC 3986 format). This should help someone learn how to use your service, including any specifics required to access data, e.g. authentication.",
example="https://docs.myservice.example.com",
)
createdAt: Optional[datetime] = Field(
None,
description="Timestamp describing when the service was first deployed and available (RFC 3339 format)",
example="2019-06-04T12:58:19Z",
)
updatedAt: Optional[datetime] = Field(
None,
description="Timestamp describing when the service was last updated (RFC 3339 format)",
example="2019-06-04T12:58:19Z",
)
environment: Optional[str] = Field(
None,
description="Environment the service is running in. Use this to distinguish between production, development and testing/staging deployments. Suggested values are prod, test, dev, staging. However this is advised and not enforced.",
example="test",
)
version: str = Field(
...,
description="Version of the service being described. Semantic versioning is recommended, but other identifiers, such as dates or commit hashes, are also allowed. The version should be changed whenever the service is updated.",
example="1.0.0",
)
+24
View File
@@ -0,0 +1,24 @@
from typing import NamedTuple
class EncodedIdentifier(NamedTuple):
tool_shed_base: str
encoded_id: str
# TRS specified encoding/decoding according to...
# https://datatracker.ietf.org/doc/html/rfc3986#section-2.4
# Failed to get whole tool shed IDs working with FastAPI
# - https://github.com/tiangolo/fastapi/issues/791#issuecomment-742799299
# - urllib.parse.quote(identifier, safe='') will produce the URL fragements but
# but FastAPI eat them.
def decode_identifier(tool_shed_base: str, quoted_tool_id: str) -> str:
suffix = "/".join(quoted_tool_id.split("~"))
return f"{tool_shed_base}/repos/{suffix}"
def encode_identifier(identifier: str) -> EncodedIdentifier:
base, rest = identifier.split("/repos/", 1)
return EncodedIdentifier(base, "~".join(rest.split("/")))
+2
View File
@@ -202,3 +202,5 @@ relative-imports-order = "closest-to-furthest"
# Don't check some pyupgrade rules on generated files
"lib/galaxy/schema/bco/*" = ["UP006", "UP007"]
"lib/galaxy/schema/drs/*" = ["UP006", "UP007"]
"lib/tool_shed_client/schema/trs.py" = ["UP006", "UP007"]
"lib/tool_shed_client/schema/trs_service_info.py" = ["UP006", "UP007"]
+21
View File
@@ -0,0 +1,21 @@
from tool_shed.context import ProvidesRepositoriesContext
from tool_shed.managers.trs import get_tool
from tool_shed.webapp.model import Repository
from tool_shed_client.schema.trs import Tool
from ._util import upload_directories_to_repository
def test_get_tool(provides_repositories: ProvidesRepositoriesContext, new_repository: Repository):
upload_directories_to_repository(provides_repositories, new_repository, "column_maker")
owner = new_repository.user.username
name = new_repository.name
encoded_id = f"{owner}~{name}~Add_a_column1"
tool: Tool = get_tool(provides_repositories, encoded_id)
assert tool
assert tool.organization == owner
assert tool.id == encoded_id
assert tool.aliases
assert tool.aliases[0] == f"localhost/repos/{owner}/{name}/Add_a_column1"
tool_versions = tool.versions
assert len(tool_versions) == 3