from typing import ( Any, Literal, Optional, Union, ) from pydantic import ( BaseModel, ConfigDict, Field, RootModel, ) from typing_extensions import ( TypedDict, ) from galaxy.tool_util_models import ParsedTool class Repository(BaseModel): # element/collection view on the backend have same keys/impl id: str name: str owner: str type: str # TODO: enum remote_repository_url: Optional[str] = None homepage_url: Optional[str] = None description: str user_id: str private: bool deleted: bool times_downloaded: int deprecated: bool create_time: str update_time: str class DetailedRepository(Repository): long_description: Optional[str] class RepositoryPermissions(BaseModel): allow_push: list[str] can_manage: bool # can the requesting user manage the repository can_push: bool class RepositoryRevisionReadmes(RootModel): root: dict[str, str] class CreateUserRequest(BaseModel): username: str email: str password: str class User(BaseModel): id: str username: str class UserV2(User): is_admin: bool class Category(BaseModel): id: str name: str description: str deleted: bool repositories: int class CreateCategoryRequest(BaseModel): name: str description: Optional[str] = None class ValidRepostiroyUpdateMessage(BaseModel): message: str class FailedRepositoryUpdateMessage(BaseModel): err_msg: str class GetOrderedInstallableRevisionsRequest(BaseModel): name: str owner: str class OrderedInstallableRevisions(RootModel): root: list[str] RepositoryType = Literal[ "repository_suite_definition", "tool_dependency_definition", "unrestricted", ] class CreateRepositoryRequest(BaseModel): name: str synopsis: str description: Optional[str] = None remote_repository_url: Optional[str] = None homepage_url: Optional[str] = None type_: RepositoryType = Field( "unrestricted", alias="type", title="Type", ) category_ids: Optional[Union[list[str], str]] = Field( ..., alias="category_ids[]", title="Category IDs", ) model_config = ConfigDict(populate_by_name=True) class UpdateRepositoryRequest(BaseModel): name: Optional[str] = None synopsis: Optional[str] = None type_: Optional[RepositoryType] = Field( None, alias="type", title="Type", ) description: Optional[str] = None remote_repository_url: Optional[str] = None homepage_url: Optional[str] = None category_ids: Optional[list[str]] = Field( None, alias="category_ids", title="Category IDs", ) model_config = ConfigDict(populate_by_name=True) class RepositoryUpdateRequest(BaseModel): commit_message: Optional[str] = None class RepositoryUpdate(RootModel): root: Union[ValidRepostiroyUpdateMessage, FailedRepositoryUpdateMessage] @property def is_ok(self): return isinstance(self.root, ValidRepostiroyUpdateMessage) class RepositoryTool(BaseModel): # Added back in post v2 in order for the frontend to render # tool descriptions on the repository page. description: str guid: str id: str name: str requirements: list tool_config: str tool_type: str version: str # add_to_tool_panel: bool # tests: list # version_string_cmd: Optional[str] class InvalidTool(BaseModel): tool_config: str error_message: str class RepositoryRevisionMetadata(BaseModel): id: str repository: Repository repository_dependencies: list["RepositoryDependency"] tools: Optional[list["RepositoryTool"]] = None invalid_tools: list[InvalidTool] repository_id: str numeric_revision: int changeset_revision: str malicious: bool downloadable: bool missing_test_components: bool has_repository_dependencies: bool includes_tools: bool includes_tools_for_display_in_tool_panel: bool # Deprecate these... includes_tool_dependencies: Optional[bool] = None includes_datatypes: Optional[bool] = None includes_workflows: Optional[bool] = None class RepositoryDependency(RepositoryRevisionMetadata): # This only needs properties for tests it seems? # e.g. test_0550_metadata_updated_dependencies.py pass class RepositoryMetadata(RootModel): root: dict[str, RepositoryRevisionMetadata] @property def latest_revision(self) -> RepositoryRevisionMetadata: return list(self.root.values())[-1] @property def tip(self) -> str: if self.is_new: return "000000000000" else: return self.latest_revision.changeset_revision @property def is_new(self) -> bool: return len(self.root) == 0 class RepositoryRevisionMetadataPreview(BaseModel): """Like RepositoryRevisionMetadata but with Optional fields for dry-run/preview scenarios. During reset_metadata dry-run, metadata objects are created in-memory but not persisted, so they lack database IDs. The numeric_revision may also be unavailable for newly-pushed changesets that haven't been indexed yet. """ id: Optional[str] = None repository: Repository repository_dependencies: list["RepositoryDependency"] tools: Optional[list["RepositoryTool"]] = None invalid_tools: list[InvalidTool] = [] repository_id: Optional[str] = None numeric_revision: Optional[int] = None changeset_revision: str malicious: bool downloadable: bool missing_test_components: bool has_repository_dependencies: bool includes_tools: bool includes_tools_for_display_in_tool_panel: bool includes_tool_dependencies: Optional[bool] = None includes_datatypes: Optional[bool] = None includes_workflows: Optional[bool] = None class RepositoryMetadataPreview(RootModel): """Like RepositoryMetadata but uses RepositoryRevisionMetadataPreview for dry-run scenarios.""" root: dict[str, RepositoryRevisionMetadataPreview] class ResetMetadataOnRepositoryRequest(BaseModel): repository_id: str dry_run: bool = False verbose: bool = False class ChangesetMetadataStatus(BaseModel): """Per-changeset detail during reset metadata operation.""" changeset_revision: str numeric_revision: int comparison_result: Optional[str] = None # "initial", "equal", "subset", "not_equal_and_not_subset", "no_metadata" record_operation: Optional[Literal["created", "updated"]] = None has_tools: bool = False has_repository_dependencies: bool = False has_tool_dependencies: bool = False error: Optional[str] = None class ResetMetadataOnRepositoryResponse(BaseModel): status: str # TODO: enum... repository_status: list[str] start_time: str stop_time: str dry_run: bool = False changeset_details: Optional[list[ChangesetMetadataStatus]] = None # Full metadata snapshots for diffing (only when verbose=True) # Uses Preview types since dry-run objects may lack IDs repository_metadata_before: Optional["RepositoryMetadataPreview"] = None repository_metadata_after: Optional["RepositoryMetadataPreview"] = None # Ugh - use with care - param descriptions scraped from older version of the API. class ResetMetadataOnRepositoriesRequest(BaseModel): my_writable: bool = Field( False, description="""if the API key is associated with an admin user in the Tool Shed, setting this param value to True will restrict resetting metadata to only repositories that are writable by the user in addition to those repositories of type tool_dependency_definition. This param is ignored if the current user is not an admin user, in which case this same restriction is automatic.""", ) encoded_ids_to_skip: Optional[list[str]] = Field( None, description="a list of encoded repository ids for repositories that should not be processed" ) class ResetMetadataOnRepositoriesResponse(BaseModel): repository_status: list[str] start_time: str stop_time: str class ToolSearchRequest(BaseModel): q: str page: Optional[int] = None page_size: Optional[int] = None class ToolSearchHitTool(BaseModel): id: str repo_owner_username: str repo_name: str name: str description: str class ToolSearchHit(BaseModel): tool: ToolSearchHitTool matched_terms: dict[str, Any] score: float class ToolSearchResults(BaseModel): # These next three really should be ints :< total_results: str page: str page_size: str hostname: str hits: list[ToolSearchHit] def find_search_hit(self, repository: Repository) -> Optional[ToolSearchHit]: matching_hit: Optional[ToolSearchHit] = None for hit in self.hits: owner_matches = hit.tool.repo_owner_username == repository.owner name_matches = hit.tool.repo_name == repository.name if owner_matches and name_matches: matching_hit = hit break return matching_hit IndexSortByType = Literal["name", "create_time"] class RepositoryIndexRequest(BaseModel): filter: Optional[str] = None owner: Optional[str] = None name: Optional[str] = None deleted: str = "false" category_id: Optional[str] = None sort_by: Optional[IndexSortByType] = "name" sort_desc: Optional[bool] = False class RepositoryPaginatedIndexRequest(RepositoryIndexRequest): page: int = 1 page_size: int = 10 class RepositoriesByCategory(BaseModel): id: str name: str description: str repository_count: int repositories: list[Repository] class RepositoryIndexResponse(RootModel): root: list[Repository] class RepositorySearchRequest(BaseModel): q: str page: Optional[int] = None page_size: Optional[int] = None class RepositorySearchResult(BaseModel): id: str name: str repo_owner_username: str description: str long_description: Optional[str] = None remote_repository_url: Optional[str] = None homepage_url: Optional[str] = None last_update: Optional[str] = None full_last_updated: str repo_lineage: str approved: bool times_downloaded: int categories: str class RepositorySearchHit(BaseModel): score: float repository: RepositorySearchResult class RepositorySearchResults(BaseModel): total_results: str page: str page_size: str hostname: str hits: list[RepositorySearchHit] # align with the search version of this to some degree but fix some things also class PaginatedRepositoryIndexResults(BaseModel): total_results: int page: int page_size: int hostname: str hits: list[Repository] class GetInstallInfoRequest(BaseModel): owner: str name: str changeset_revision: str class ValidToolDict(TypedDict): add_to_tool_panel: bool description: str guid: str id: str name: str requirements: list tests: list tool_config: str tool_type: str version: str version_string_cmd: Optional[str] class RepositoryMetadataInstallInfoDict(TypedDict): changeset_revision: str downloadable: bool has_repository_dependencies: bool has_repository_dependencies_only_if_compiling_contained_td: bool id: str includes_datatypes: bool includes_tool_dependencies: bool includes_tools: bool includes_tools_for_display_in_tool_panel: bool includes_workflows: bool malicious: bool repository_id: str url: str valid_tools: list[ValidToolDict] # So hard to type this... the keys are repo names and the elements # are tuples that have been list-ified. ExtraRepoInfo = dict[str, list] # { # "add_column": [ # "add_column hello", # "http://test@localhost:9009/repos/test/add_column", # "3a08cc21466f", # "1", # "test", # {}, # {} # ] # } class EmptyDict(TypedDict): pass LegacyInstallInfoTuple = tuple[ Optional[dict], Union[RepositoryMetadataInstallInfoDict, EmptyDict], Union[ExtraRepoInfo, EmptyDict] ] class RepositoryExtraInstallInfo(BaseModel): name: str description: str repository_clone_url: str changeset_revision: str ctx_rev: str repository_owner: str repository_dependencies: Optional[dict] = None # tool dependencies not longer work so don't transmit them in v2? # tool_dependencies: Optional[Dict] @staticmethod def from_legacy_dict(as_dict: ExtraRepoInfo) -> "RepositoryExtraInstallInfo": assert len(as_dict) == 1 repo_name = next(iter(as_dict.keys())) info_indexable = as_dict[repo_name] return RepositoryExtraInstallInfo( name=repo_name, description=info_indexable[0], repository_clone_url=info_indexable[1], changeset_revision=info_indexable[2], ctx_rev=info_indexable[3], repository_owner=info_indexable[4], repository_dependencies=info_indexable[5], ) class ValidTool(BaseModel): add_to_tool_panel: bool description: str guid: str id: str name: str requirements: list tests: list tool_config: str tool_type: str version: str version_string_cmd: Optional[str] = None @staticmethod def from_legacy_dict(as_dict: ValidToolDict) -> "ValidTool": return ValidTool(**as_dict) @staticmethod def from_legacy_list(as_dicts: list[ValidToolDict]) -> list["ValidTool"]: return [ValidTool.from_legacy_dict(d) for d in as_dicts] class RepositoryMetadataInstallInfo(BaseModel): id: str changeset_revision: str downloadable: bool has_repository_dependencies: bool includes_tools: bool includes_tools_for_display_in_tool_panel: bool malicious: bool repository_id: str url: str valid_tools: list[ValidTool] # no longer used, don't transmit. # has_repository_dependencies_only_if_compiling_contained_td: bool # includes_datatypes: bool # includes_tool_dependencies: bool # includes_workflows: bool @staticmethod def from_legacy_dict(as_dict: RepositoryMetadataInstallInfoDict) -> "RepositoryMetadataInstallInfo": return RepositoryMetadataInstallInfo( id=as_dict["id"], changeset_revision=as_dict["changeset_revision"], downloadable=as_dict["downloadable"], has_repository_dependencies=as_dict["has_repository_dependencies"], includes_tools=as_dict["includes_tools"], includes_tools_for_display_in_tool_panel=as_dict["includes_tools_for_display_in_tool_panel"], malicious=as_dict["malicious"], repository_id=as_dict["repository_id"], url=as_dict["url"], valid_tools=ValidTool.from_legacy_list(as_dict.get("valid_tools", [])), ) class InstallInfo(BaseModel): metadata_info: Optional[RepositoryMetadataInstallInfo] = None repo_info: Optional[RepositoryExtraInstallInfo] = None def from_legacy_install_info(legacy_install_info: LegacyInstallInfoTuple) -> InstallInfo: repo_metadata_install_info: Union[RepositoryMetadataInstallInfoDict, EmptyDict] extra_info: Union[ExtraRepoInfo, EmptyDict] _, repo_metadata_install_info, extra_info = legacy_install_info if repo_metadata_install_info: metadata_info = RepositoryMetadataInstallInfo.from_legacy_dict(repo_metadata_install_info) else: metadata_info = None if extra_info: repo_info = RepositoryExtraInstallInfo.from_legacy_dict(extra_info) else: repo_info = None return InstallInfo( metadata_info=metadata_info, repo_info=repo_info, ) class BuildSearchIndexResponse(BaseModel): repositories_indexed: int tools_indexed: int class Version(BaseModel): version_major: str version: str api_version: str = "v1" class ShedParsedTool(ParsedTool): repository_revision: Optional[RepositoryRevisionMetadata] = None