diff --git a/lib/galaxy/managers/metrics.py b/lib/galaxy/managers/metrics.py index 2ff017edf05..f3997e1165a 100644 --- a/lib/galaxy/managers/metrics.py +++ b/lib/galaxy/managers/metrics.py @@ -18,12 +18,7 @@ from galaxy.structured_app import MinimalManagerApp log = logging.getLogger(__name__) -def datetime_to_iso8601(utc_datetime: datetime) -> str: - """ - Returns the date as string with ISO8601 format. - i.e: 2021-01-23T18:25:43.511Z - """ - return f"{utc_datetime.isoformat()}Z" +SOME_EXAMPLE_DATE = "2021-01-23T18:25:43.511Z" class Metric(BaseModel): @@ -36,7 +31,7 @@ class Metric(BaseModel): ..., # Required title="Timestamp", description="The timestamp in ISO format.", - example=datetime_to_iso8601(datetime.utcnow()), + example=SOME_EXAMPLE_DATE, ) level: int = Field( ..., # Required @@ -54,11 +49,7 @@ class CreateMetricsPayload(BaseModel): metrics: List[Metric] = Field( default=[], title="List of metrics to be recorded.", - example=[ - Metric( - namespace="test-source", time=datetime_to_iso8601(datetime.utcnow()), level=0, args='{"test":"value"}' - ) - ], + example=[Metric(namespace="test-source", time=SOME_EXAMPLE_DATE, level=0, args='{"test":"value"}')], ) diff --git a/lib/galaxy/quota/_schema.py b/lib/galaxy/quota/_schema.py index 5b20d908bdf..1274d1d6724 100644 --- a/lib/galaxy/quota/_schema.py +++ b/lib/galaxy/quota/_schema.py @@ -5,6 +5,10 @@ from typing import ( ) from pydantic import Field +from typing_extensions import ( + Annotated, + Literal, +) from galaxy.schema.fields import ( DecodedDatabaseIdField, @@ -16,10 +20,10 @@ from galaxy.schema.schema import ( UserModel, ) -QUOTA_MODEL_CLASS_NAME = "Quota" -USER_QUOTA_ASSOCIATION_MODEL_CLASS_NAME = "UserQuotaAssociation" -GROUP_QUOTA_ASSOCIATION_MODEL_CLASS_NAME = "GroupQuotaAssociation" -DEFAULT_QUOTA_ASSOCIATION_MODEL_CLASS_NAME = "DefaultQuotaAssociation" +QUOTA = Literal["Quota"] +USER_QUOTA_ASSOCIATION = Literal["UserQuotaAssociation"] +GROUP_QUOTA_ASSOCIATION = Literal["GroupQuotaAssociation"] +DEFAULT_QUOTA_ASSOCIATION = Literal["DefaultQuotaAssociation"] class QuotaOperation(str, Enum): @@ -66,7 +70,7 @@ QuotaOperationField = Field( class DefaultQuota(Model): # TODO: should this replace lib.galaxy.model.DefaultQuotaAssociation at some point? - model_class: str = ModelClassField(DEFAULT_QUOTA_ASSOCIATION_MODEL_CLASS_NAME) + model_class: Annotated[DEFAULT_QUOTA_ASSOCIATION, ModelClassField()] type: DefaultQuotaTypes = Field( ..., title="Type", @@ -79,7 +83,7 @@ class DefaultQuota(Model): # TODO: should this replace lib.galaxy.model.Default class UserQuota(Model): - model_class: str = ModelClassField(USER_QUOTA_ASSOCIATION_MODEL_CLASS_NAME) + model_class: Annotated[USER_QUOTA_ASSOCIATION, ModelClassField()] user: UserModel = Field( ..., title="User", @@ -88,7 +92,7 @@ class UserQuota(Model): class GroupQuota(Model): - model_class: str = ModelClassField(GROUP_QUOTA_ASSOCIATION_MODEL_CLASS_NAME) + model_class: Annotated[GROUP_QUOTA_ASSOCIATION, ModelClassField()] group: GroupModel = Field( ..., title="Group", @@ -99,7 +103,7 @@ class GroupQuota(Model): class QuotaBase(Model): """Base model containing common fields for Quotas.""" - model_class: str = ModelClassField(QUOTA_MODEL_CLASS_NAME) + model_class: Annotated[QUOTA, ModelClassField()] id: DecodedDatabaseIdField = Field( ..., title="ID", diff --git a/lib/galaxy/schema/fields.py b/lib/galaxy/schema/fields.py index 46aead9826f..4dca255c4ad 100644 --- a/lib/galaxy/schema/fields.py +++ b/lib/galaxy/schema/fields.py @@ -94,15 +94,13 @@ class EncodedDatabaseIdField(str, BaseDatabaseIdField): return cls.security.decode_id(v) -def ModelClassField(class_name: str) -> str: +def ModelClassField(): """Represents a database model class name annotated as a constant pydantic Field. :param class_name: The name of the database class. :return: A constant pydantic Field with default annotations for model classes. """ return Field( - class_name, title="Model class", description="The name of the database model class.", - const=True, # Make this field constant ) diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index c047ef6014a..c0cd7cef856 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -50,15 +50,16 @@ from galaxy.schema.types import ( RelativeUrl, ) -USER_MODEL_CLASS_NAME = "User" -GROUP_MODEL_CLASS_NAME = "Group" -HDA_MODEL_CLASS_NAME = "HistoryDatasetAssociation" -DC_MODEL_CLASS_NAME = "DatasetCollection" -DCE_MODEL_CLASS_NAME = "DatasetCollectionElement" -HDCA_MODEL_CLASS_NAME = "HistoryDatasetCollectionAssociation" -HISTORY_MODEL_CLASS_NAME = "History" -JOB_MODEL_CLASS_NAME = "Job" -STORED_WORKFLOW_MODEL_CLASS_NAME = "StoredWorkflow" +USER_MODEL_CLASS = Literal["User"] +GROUP_MODEL_CLASS = Literal["Group"] +HDA_MODEL_CLASS = Literal["HistoryDatasetAssociation"] +DC_MODEL_CLASS = Literal["DatasetCollection"] +DCE_MODEL_CLASS = Literal["DatasetCollectionElement"] +HDCA_MODEL_CLASS = Literal["HistoryDatasetCollectionAssociation"] +HISTORY_MODEL_CLASS = Literal["History"] +JOB_MODEL_CLASS = Literal["Job"] +STORED_WORKFLOW_MODEL_CLASS = Literal["StoredWorkflow"] +PAGE_MODEL_CLASS = Literal["Page"] # Generic and common Field annotations that can be reused across models @@ -204,13 +205,13 @@ class UserModel(Model): active: bool = Field(title="Active", description="User is active") deleted: bool = Field(title="Deleted", description="User is deleted") last_password_change: Optional[datetime] = Field(title="Last password change", description="") - model_class: str = ModelClassField(USER_MODEL_CLASS_NAME) + model_class: Annotated[USER_MODEL_CLASS, ModelClassField()] class GroupModel(Model): """User group model""" - model_class: str = ModelClassField(GROUP_MODEL_CLASS_NAME) + model_class: Annotated[GROUP_MODEL_CLASS, ModelClassField()] id: DecodedDatabaseIdField = Field( ..., # Required title="ID", @@ -448,7 +449,7 @@ HdaLddaField = Field( class HDADetailed(HDASummary): """History Dataset Association detailed information.""" - model_class: str = ModelClassField(HDA_MODEL_CLASS_NAME) + model_class: Annotated[HDA_MODEL_CLASS, ModelClassField()] hda_ldda: DatasetSourceType = HdaLddaField accessible: bool = AccessibleField genome_build: Optional[str] = GenomeBuildField @@ -557,19 +558,21 @@ class HDADetailed(HDASummary): ) annotation: Optional[str] = AnnotationField download_url: RelativeUrl = DownloadUrlField - type: str = Field( - "file", - const=True, - title="Type", - description="This is always `file` for datasets.", - ) - api_type: str = Field( - "file", - const=True, - title="API Type", - description="TODO", - deprecated=False, # TODO: Should this field be deprecated as announced in release 16.04? - ) + type: Annotated[ + Literal["file"], + Field( + title="Type", + description="This is always `file` for datasets.", + ), + ] = "file" + api_type: Annotated[ + Literal["file"], + Field( + title="API Type", + description="TODO", + deprecated=False, # TODO: Should this field be deprecated as announced in release 16.04? + ), + ] = "file" created_from_basename: Optional[str] = Field( None, title="Created from basename", @@ -600,7 +603,7 @@ class HDAExtended(HDADetailed): class DCSummary(Model): """Dataset Collection summary information.""" - model_class: str = ModelClassField(DC_MODEL_CLASS_NAME) + model_class: Annotated[DC_MODEL_CLASS, ModelClassField()] id: DecodedDatabaseIdField = EntityIdField create_time: datetime = CreateTimeField update_time: datetime = UpdateTimeField @@ -614,7 +617,7 @@ class HDAObject(Model): """History Dataset Association Object""" id: DecodedDatabaseIdField = EntityIdField - model_class: str = ModelClassField(HDA_MODEL_CLASS_NAME) + model_class: Annotated[HDA_MODEL_CLASS, ModelClassField()] state: Dataset.states = DatasetStateField hda_ldda: DatasetSourceType = HdaLddaField history_id: DecodedDatabaseIdField = HistoryIdField @@ -627,7 +630,7 @@ class DCObject(Model): """Dataset Collection Object""" id: DecodedDatabaseIdField = EntityIdField - model_class: str = ModelClassField(DC_MODEL_CLASS_NAME) + model_class: Annotated[DC_MODEL_CLASS, ModelClassField()] collection_type: CollectionType = CollectionTypeField populated: Optional[bool] = PopulatedField element_count: Optional[int] = ElementCountField @@ -639,7 +642,7 @@ class DCESummary(Model): """Dataset Collection Element summary information.""" id: DecodedDatabaseIdField = EntityIdField - model_class: str = ModelClassField(DCE_MODEL_CLASS_NAME) + model_class: Annotated[DCE_MODEL_CLASS, ModelClassField()] element_index: int = Field( ..., title="Element Index", @@ -745,15 +748,14 @@ class HDCJobStateSummary(Model): class HDCASummary(HistoryItemCommon): """History Dataset Collection Association summary information.""" - model_class: str = ModelClassField( - HDCA_MODEL_CLASS_NAME - ) # TODO: inconsistency? HDASummary does not have model_class only the detailed view has it... - type: str = Field( - "collection", - const=True, - title="Type", - description="This is always `collection` for dataset collections.", - ) + model_class: Annotated[HDCA_MODEL_CLASS, ModelClassField()] + type: Annotated[ + Literal["collection"], + Field( + title="Type", + description="This is always `collection` for dataset collections.", + ), + ] = "collection" collection_type: CollectionType = CollectionTypeField populated_state: DatasetCollection.populated_states = PopulatedStateField populated_state_message: Optional[str] = PopulatedStateMessageField @@ -904,7 +906,7 @@ class UpdateHistoryContentsPayload(HistoryBase): class HistorySummary(HistoryBase): """History summary information.""" - model_class: str = ModelClassField(HISTORY_MODEL_CLASS_NAME) + model_class: Annotated[HISTORY_MODEL_CLASS, ModelClassField()] id: DecodedDatabaseIdField = EntityIdField name: str = Field( ..., @@ -1467,7 +1469,7 @@ class JobIdResponse(Model): class JobBaseModel(Model): id: DecodedDatabaseIdField = EntityIdField - model_class: str = ModelClassField(JOB_MODEL_CLASS_NAME) + model_class: Annotated[JOB_MODEL_CLASS, ModelClassField()] tool_id: str = Field( ..., title="Tool ID", @@ -1506,21 +1508,24 @@ class JobImportHistoryResponse(JobBaseModel): ) -class JobStateSummary(Model): +class ItemStateSummary(Model): id: DecodedDatabaseIdField = EntityIdField - model: str = ModelClassField("Job") populated_state: DatasetCollection.populated_states = PopulatedStateField states: Dict[Job.states, int] = Field( {}, title="States", description=("A dictionary of job states and the number of jobs in that state.") ) -class ImplicitCollectionJobsStateSummary(JobStateSummary): - model: str = ModelClassField("ImplicitCollectionJobs") +class JobStateSummary(ItemStateSummary): + model: Annotated[Literal["Job"], ModelClassField()] -class WorkflowInvocationStateSummary(JobStateSummary): - model: str = ModelClassField("WorkflowInvocation") +class ImplicitCollectionJobsStateSummary(ItemStateSummary): + model: Annotated[Literal["ImplicitCollectionJobs"], ModelClassField()] + + +class WorkflowInvocationStateSummary(ItemStateSummary): + model: Annotated[Literal["WorkflowInvocation"], ModelClassField()] class JobSummary(JobBaseModel): @@ -1687,7 +1692,7 @@ class JobFullDetails(JobDetails): class StoredWorkflowSummary(Model): id: DecodedDatabaseIdField = EntityIdField - model_class: str = ModelClassField(STORED_WORKFLOW_MODEL_CLASS_NAME) + model_class: Annotated[STORED_WORKFLOW_MODEL_CLASS, ModelClassField()] create_time: datetime = CreateTimeField update_time: datetime = UpdateTimeField name: str = Field( @@ -2176,7 +2181,7 @@ class BasicRoleModel(Model): class RoleModel(BasicRoleModel): description: Optional[str] = RoleDescriptionField url: RelativeUrl = RelativeUrlField - model_class: str = ModelClassField("Role") + model_class: Annotated[Literal["Role"], ModelClassField()] class RoleDefinitionModel(Model): @@ -2252,7 +2257,7 @@ class InstalledRepositoryToolShedStatus(Model): class InstalledToolShedRepository(Model): - model_class: str = ModelClassField("ToolShedRepository") + model_class: Annotated[Literal["ToolShedRepository"], ModelClassField()] = "ToolShedRepository" id: EncodedDatabaseIdField = Field( ..., title="ID", @@ -2306,7 +2311,7 @@ class LibraryPermissionScope(str, Enum): class LibraryLegacySummary(Model): - model_class: str = ModelClassField("Library") + model_class: Annotated[Literal["Library"], ModelClassField()] id: DecodedDatabaseIdField = Field( ..., title="ID", @@ -2553,7 +2558,7 @@ class LibraryFolderPermissionsPayload(LibraryPermissionsPayloadBase): class LibraryFolderDetails(Model): - model_class: str = ModelClassField("LibraryFolder") + model_class: Annotated[Literal["LibraryFolder"], ModelClassField()] id: LibraryFolderDatabaseIdField = Field( ..., title="ID", @@ -2811,12 +2816,16 @@ AnyHistoryContentItem = Union[ ] -AnyJobStateSummary = Union[ - JobStateSummary, - ImplicitCollectionJobsStateSummary, - WorkflowInvocationStateSummary, +AnyJobStateSummary = Annotated[ + Union[ + JobStateSummary, + ImplicitCollectionJobsStateSummary, + WorkflowInvocationStateSummary, + ], + Field(..., discriminator="model"), ] + HistoryArchiveExportResult = Union[JobExportHistoryArchiveModel, JobIdResponse] @@ -3202,12 +3211,7 @@ class PageSummary(PageSummaryBase): title="ID", description="Encoded ID of the Page.", ) - model_class: str = Field( - ..., # Required - title="Model class", - description="The class of the model associated with the ID.", - example="Page", - ) + model_class: Annotated[PAGE_MODEL_CLASS, ModelClassField()] username: str = Field( ..., # Required title="Username", diff --git a/lib/galaxy/webapps/galaxy/services/dataset_collections.py b/lib/galaxy/webapps/galaxy/services/dataset_collections.py index 93e5a7ae275..03b58b17897 100644 --- a/lib/galaxy/webapps/galaxy/services/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/services/dataset_collections.py @@ -12,6 +12,10 @@ from pydantic import ( Field, ValidationError, ) +from typing_extensions import ( + Annotated, + Literal, +) from galaxy import exceptions from galaxy.datatypes.registry import Registry @@ -64,7 +68,7 @@ class DatasetCollectionAttributesResult(Model): dbkey: str = Field(..., description="TODO") # Are the following fields really used/needed? extension: str = Field(..., description="The dataset file extension.", example="txt") - model_class: str = ModelClassField("HistoryDatasetCollectionAssociation") + model_class: Annotated[Literal["HistoryDatasetCollectionAssociation"], ModelClassField()] dbkeys: Optional[Set[str]] extensions: Optional[Set[str]] tags: TagCollection