diff --git a/client-api/src/api-types.ts b/client-api/src/api-types.ts index e22000febf0a..07544c435106 100644 --- a/client-api/src/api-types.ts +++ b/client-api/src/api-types.ts @@ -26,7 +26,7 @@ export type DatasetStorageDetails = components["schemas"]["DatasetStorageDetails export type DatasetCollectionAttributes = components["schemas"]["DatasetCollectionAttributesResult"]; export type ConcreteObjectStoreModel = components["schemas"]["ConcreteObjectStoreModel"]; export type MessageException = components["schemas"]["MessageExceptionModel"]; -export type DatasetHash = components["schemas"]["DatasetHash-Output"]; +export type DatasetHash = components["schemas"]["DatasetHash"]; export type DatasetSource = components["schemas"]["DatasetSource"]; export type DatasetTransform = components["schemas"]["DatasetSourceTransform"]; export type StoreExportPayload = components["schemas"]["StoreExportPayload"]; diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 3bf519095574..4f2ea81c5682 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -308,7 +308,7 @@ export function canMutateHistory(history: AnyHistory): boolean { return !history.purged && !history.archived; } -export type DatasetHash = components["schemas"]["DatasetHash-Output"]; +export type DatasetHash = components["schemas"]["DatasetHash"]; export type DatasetSource = components["schemas"]["DatasetSource"]; export type DatasetTransform = components["schemas"]["DatasetSourceTransform"]; diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 29800bfa8dcc..09c4d57b37e6 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -7659,7 +7659,7 @@ export interface components { /** Ext */ ext: string; /** Hashes */ - hashes?: components["schemas"]["DatasetHash-Input"][] | null; + hashes?: components["schemas"]["FileHash"][] | null; /** * Identifier * @description A unique identifier for this element within the collection. @@ -10101,17 +10101,7 @@ export interface components { */ DatasetExtraFiles: components["schemas"]["ExtraFileEntry"][]; /** DatasetHash */ - "DatasetHash-Input": { - /** - * Hash Function - * @enum {string} - */ - hash_function: "MD5" | "SHA-1" | "SHA-256" | "SHA-512"; - /** Hash Value */ - hash_value: string; - }; - /** DatasetHash */ - "DatasetHash-Output": { + DatasetHash: { /** * Extra Files Path * @description The path to the extra files used to generate the hash. @@ -11730,6 +11720,16 @@ export interface components { */ action_type: "fill_defaults"; }; + /** FileHash */ + FileHash: { + /** + * Hash Function + * @enum {string} + */ + hash_function: "MD5" | "SHA-1" | "SHA-256" | "SHA-512"; + /** Hash Value */ + hash_value: string; + }; /** FileLibraryFolderItem */ FileLibraryFolderItem: { /** Can Manage */ @@ -11855,7 +11855,7 @@ export interface components { /** Ext */ ext: string; /** Hashes */ - hashes?: components["schemas"]["DatasetHash-Input"][] | null; + hashes?: components["schemas"]["FileHash"][] | null; /** Info */ info?: string | null; /** Location */ @@ -12725,7 +12725,7 @@ export interface components { * Hashes * @description The list of hashes associated with this dataset. */ - hashes?: components["schemas"]["DatasetHash-Output"][] | null; + hashes?: components["schemas"]["DatasetHash"][] | null; /** * HDA or LDDA * @description Whether this dataset belongs to a history (HDA) or a library (LDDA). @@ -12999,7 +12999,7 @@ export interface components { * Hashes * @description The list of hashes associated with this dataset. */ - hashes: components["schemas"]["DatasetHash-Output"][]; + hashes: components["schemas"]["DatasetHash"][]; /** * HDA or LDDA * @description Whether this dataset belongs to a history (HDA) or a library (LDDA). diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt index 340ff4c51761..e5a3ff3a1558 100644 --- a/lib/galaxy/dependencies/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pinned-requirements.txt @@ -11,6 +11,7 @@ aiosignal==1.4.0 alembic==1.16.5 ; python_full_version < '3.10' alembic==1.17.2 ; python_full_version >= '3.10' amqp==5.3.1 +annotated-doc==0.0.4 annotated-types==0.7.0 anyio==4.12.0 apispec==6.8.4 @@ -70,7 +71,7 @@ edam-ontology==1.25.3 email-validator==2.3.0 et-xmlfile==2.0.0 exceptiongroup==1.3.1 -fastapi==0.118.3 +fastapi==0.124.0 filelock==3.19.1 ; python_full_version < '3.10' filelock==3.20.0 ; python_full_version >= '3.10' fissix==24.4.24 @@ -213,7 +214,8 @@ sortedcontainers==2.4.0 spython==0.3.14 sqlalchemy==2.0.44 sqlparse==0.5.4 -starlette==0.48.0 +starlette==0.49.3 ; python_full_version < '3.10' +starlette==0.50.0 ; python_full_version >= '3.10' starlette-context==0.4.0 supervisor==4.3.0 svgwrite==1.4.3 diff --git a/lib/galaxy/tool_util_models/parameters.py b/lib/galaxy/tool_util_models/parameters.py index 135e5806291f..9d20e3cdbf7c 100644 --- a/lib/galaxy/tool_util_models/parameters.py +++ b/lib/galaxy/tool_util_models/parameters.py @@ -402,7 +402,7 @@ class DataRequestHdca(LegacyRequestModelAttributes): id: StrictStr -class DatasetHash(StrictModel): +class FileHash(StrictModel): hash_function: Literal["MD5", "SHA-1", "SHA-256", "SHA-512"] hash_value: StrictStr @@ -416,7 +416,7 @@ class BaseDataRequest(StrictModel): created_from_basename: Optional[StrictStr] = None info: Optional[StrictStr] = None tags: Optional[List[str]] = None - hashes: Optional[List[DatasetHash]] = None + hashes: Optional[List[FileHash]] = None space_to_tab: bool = False to_posix_lines: bool = False diff --git a/lib/galaxy/util/config_templates.py b/lib/galaxy/util/config_templates.py index 996dcf818e76..b0b35e2df8a3 100644 --- a/lib/galaxy/util/config_templates.py +++ b/lib/galaxy/util/config_templates.py @@ -641,8 +641,6 @@ def _make_field_optional(field_info: FieldInfo): """Returns the field's definition to be used in a `create_model()` call to make the field optional.""" annotation = field_info.annotation assert annotation is not None - if isinstance(annotation, type) and issubclass(annotation, BaseModel): - annotation = make_model_with_all_fields_optional(annotation) if field_info.is_required(): return Annotated[Union[annotation, None], field_info], None else: diff --git a/lib/galaxy/webapps/galaxy/api/users.py b/lib/galaxy/webapps/galaxy/api/users.py index 7c89dd51ca2c..0cd485d81209 100644 --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -10,7 +10,6 @@ Annotated, Any, Optional, - TYPE_CHECKING, Union, ) @@ -92,9 +91,7 @@ ) from galaxy.webapps.galaxy.api.common import UserIdPathParam from galaxy.webapps.galaxy.services.users import UsersService - -if TYPE_CHECKING: - from galaxy.work.context import SessionRequestContext +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -171,7 +168,7 @@ class FastAPIUsers: ) def recalculate_disk_usage( self, - trans: "SessionRequestContext" = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ): """This route will be removed in a future version. @@ -193,7 +190,7 @@ def recalculate_disk_usage( def recalculate_disk_usage_by_user_id( self, user_id: UserIdPathParam, - trans: "SessionRequestContext" = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ): result = self.service.recalculate_disk_usage(trans, user_id) return Response(status_code=status.HTTP_204_NO_CONTENT) if result is None else result diff --git a/lib/galaxy/webapps/openapi/_compat/__init__.py b/lib/galaxy/webapps/openapi/_compat/__init__.py new file mode 100644 index 000000000000..0b03aebab7e8 --- /dev/null +++ b/lib/galaxy/webapps/openapi/_compat/__init__.py @@ -0,0 +1,5 @@ +from .main import get_definitions as get_definitions + +__all__ = [ + "get_definitions", +] diff --git a/lib/galaxy/webapps/openapi/_compat/main.py b/lib/galaxy/webapps/openapi/_compat/main.py new file mode 100644 index 000000000000..1991a9137422 --- /dev/null +++ b/lib/galaxy/webapps/openapi/_compat/main.py @@ -0,0 +1,62 @@ +import sys +from typing import ( + Any, + Union, +) + +from fastapi._compat import ( + may_v1, + v2, +) +from fastapi._compat.model_field import ModelField +from fastapi._compat.shared import PYDANTIC_V2 +from fastapi.types import ModelNameMap +from typing_extensions import Literal + +from .v2 import ( + GenerateJsonSchema as GenerateJsonSchema, + get_definitions as v2_get_definitions, +) + + +def get_definitions( + *, + fields: list[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, + schema_generator: Union[GenerateJsonSchema, None] = None, +) -> tuple[ + dict[tuple[ModelField, Literal["validation", "serialization"]], may_v1.JsonSchemaValue], + dict[str, dict[str, Any]], +]: + if sys.version_info < (3, 14): + v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] + v1_field_maps, v1_definitions = may_v1.get_definitions( + fields=v1_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + ) + if not PYDANTIC_V2: + return v1_field_maps, v1_definitions + else: + v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_field_maps, v2_definitions = v2_get_definitions( + fields=v2_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + schema_generator=schema_generator, + ) + all_definitions = {**v1_definitions, **v2_definitions} + all_field_maps = {**v1_field_maps, **v2_field_maps} + return all_field_maps, all_definitions + + # Pydantic v1 is not supported since Python 3.14 + else: + v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] + v2_field_maps, v2_definitions = v2_get_definitions( + fields=v2_fields, + model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, + schema_generator=schema_generator, + ) + return v2_field_maps, v2_definitions diff --git a/lib/galaxy/webapps/openapi/_compat/v2.py b/lib/galaxy/webapps/openapi/_compat/v2.py new file mode 100644 index 000000000000..ad6c5ea57ca0 --- /dev/null +++ b/lib/galaxy/webapps/openapi/_compat/v2.py @@ -0,0 +1,76 @@ +from collections.abc import Sequence +from typing import ( + Any, + cast, + Union, +) + +from fastapi._compat.v2 import ( + _has_computed_fields, + _remap_definitions_and_field_mappings, + get_flat_models_from_fields, + ModelField, +) +from fastapi.openapi.constants import REF_TEMPLATE +from fastapi.types import ModelNameMap +from pydantic.fields import FieldInfo as FieldInfo +from pydantic.json_schema import ( + GenerateJsonSchema as GenerateJsonSchema, + JsonSchemaValue as JsonSchemaValue, +) +from typing_extensions import Literal + + +def get_definitions( + *, + fields: Sequence[ModelField], + model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, + schema_generator: Union[GenerateJsonSchema, None] = None, +) -> tuple[ + dict[tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], + dict[str, dict[str, Any]], +]: + schema_generator = schema_generator or GenerateJsonSchema(ref_template=REF_TEMPLATE) + validation_fields = [field for field in fields if field.mode == "validation"] + serialization_fields = [field for field in fields if field.mode == "serialization"] + flat_validation_models = get_flat_models_from_fields(validation_fields, known_models=set()) + flat_serialization_models = get_flat_models_from_fields(serialization_fields, known_models=set()) + flat_validation_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="validation", + ) + for model in flat_validation_models + ] + flat_serialization_model_fields = [ + ModelField( + field_info=FieldInfo(annotation=model), + name=model.__name__, + mode="serialization", + ) + for model in flat_serialization_models + ] + flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields + input_types = {f.type_ for f in fields} + unique_flat_model_fields = {f for f in flat_model_fields if f.type_ not in input_types} + inputs = [ + ( + field, + (field.mode if (separate_input_output_schemas or _has_computed_fields(field)) else "validation"), + field._type_adapter.core_schema, + ) + for field in list(fields) + list(unique_flat_model_fields) + ] + field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs) + for item_def in cast(dict[str, dict[str, Any]], definitions).values(): + if "description" in item_def: + item_description = cast(str, item_def["description"]).split("\f")[0] + item_def["description"] = item_description + new_mapping, new_definitions = _remap_definitions_and_field_mappings( + model_name_map=model_name_map, + definitions=definitions, # type: ignore[arg-type] + field_mapping=field_mapping, + ) + return new_mapping, new_definitions diff --git a/lib/galaxy/webapps/openapi/utils.py b/lib/galaxy/webapps/openapi/utils.py index aef58fb97b5e..e0128b208776 100644 --- a/lib/galaxy/webapps/openapi/utils.py +++ b/lib/galaxy/webapps/openapi/utils.py @@ -1,5 +1,5 @@ """ -Copy of https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/utils.py with changes from https://github.com/tiangolo/fastapi/pull/10903 +Copy of fastapi/openapi/utils.py from https://github.com/fastapi/fastapi/pull/13918 """ from collections.abc import Sequence @@ -12,10 +12,8 @@ from fastapi import routing from fastapi._compat import ( get_compat_model_name_map, - get_definitions, ) from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import REF_TEMPLATE from fastapi.openapi.models import OpenAPI from fastapi.openapi.utils import ( get_fields_from_routes, @@ -24,6 +22,8 @@ from pydantic.json_schema import GenerateJsonSchema from starlette.routing import BaseRoute +from ._compat import get_definitions + def get_openapi( *, @@ -40,6 +40,7 @@ def get_openapi( contact: Optional[dict[str, Union[str, Any]]] = None, license_info: Optional[dict[str, Union[str, Any]]] = None, separate_input_output_schemas: bool = True, + external_docs: Optional[dict[str, Any]] = None, schema_generator: Optional[GenerateJsonSchema] = None, ) -> dict[str, Any]: info: dict[str, Any] = {"title": title, "version": version} @@ -62,19 +63,17 @@ def get_openapi( operation_ids: set[str] = set() all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) model_name_map = get_compat_model_name_map(all_fields) - schema_generator = schema_generator or GenerateJsonSchema(ref_template=REF_TEMPLATE) field_mapping, definitions = get_definitions( fields=all_fields, - schema_generator=schema_generator, model_name_map=model_name_map, separate_input_output_schemas=separate_input_output_schemas, + schema_generator=schema_generator, ) for route in routes or []: if isinstance(route, routing.APIRoute): result = get_openapi_path( route=route, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -92,7 +91,6 @@ def get_openapi( result = get_openapi_path( route=webhook, operation_ids=operation_ids, - schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, separate_input_output_schemas=separate_input_output_schemas, @@ -114,4 +112,6 @@ def get_openapi( output["webhooks"] = webhook_paths if tags: output["tags"] = tags + if external_docs: + output["externalDocs"] = external_docs return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) diff --git a/packages/tool_shed/setup.cfg b/packages/tool_shed/setup.cfg index 87a1dfee1777..99f0069ac05e 100644 --- a/packages/tool_shed/setup.cfg +++ b/packages/tool_shed/setup.cfg @@ -42,7 +42,7 @@ install_requires = galaxy-web-apps a2wsgi alembic - fastapi>=0.111.0,!=0.119.*,!=0.120.*,!=0.121.*,!=0.122.*,!=0.123.0 + fastapi>=0.124.0 Mako MarkupSafe mercurial>=6.8.2 diff --git a/packages/web_apps/setup.cfg b/packages/web_apps/setup.cfg index d5602c98e8ec..9c5605e7e5e3 100644 --- a/packages/web_apps/setup.cfg +++ b/packages/web_apps/setup.cfg @@ -43,7 +43,7 @@ install_requires = apispec Babel CT3>=3.3.3 - fastapi>=0.111.0,!=0.119.*,!=0.120.*,!=0.121.*,!=0.122.*,!=0.123.0 + fastapi>=0.124.0 gunicorn httpx gxformat2 diff --git a/pyproject.toml b/pyproject.toml index b67c4eebcf9d..ebf1812eb939 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "docutils!=0.17,!=0.17.1", "dparse", "edam-ontology", - "fastapi>=0.111.0,!=0.119.*,!=0.120.*,!=0.121.*,!=0.122.*,!=0.123.0", # https://github.com/fastapi/fastapi/pull/13918 + "fastapi>=0.124.0", # https://github.com/fastapi/fastapi/pull/14453 "fissix", "fs", "future>=1.0.0", # Python 3.12 support