This is an automated email from the ASF dual-hosted git repository. yasith pushed a commit to branch feat/sdk-facade-migration in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit e4ef556f4dafc8636c0fc85b852cc05a9b510b1d Author: yasithdev <[email protected]> AuthorDate: Wed Apr 8 02:01:25 2026 -0500 fix: resolve ty type errors, add AiravataRequest type stub - Create django_airavata/types.py with AiravataRequest extending HttpRequest - Fix method override signatures in proto_utils.py and serializers.py to match parent parameter names (value/data) - Add type annotations to proto_compat classes (GroupComputeResourcePreference, EnvironmentSpecificPreferences, ApplicationInterfaceDescription, GroupModel) - Add missing ResourcePermissionType.OWNER enum member - Fix real bugs: logger.exception extra={set} -> {dict}, quote(None) -> quote('') - Add assert guards for Optional->non-Optional narrowing in views.py - Widen dict type annotations to dict[str, Any] where mixed-type values stored - Add DecimalField max_digits/decimal_places defaults in proto_utils - Suppress unresolvable third-party imports with ty: ignore comments - Use Any for duck-typed request parameters in user_storage/helpers/middleware Reduces ty errors from 428 to 256 (remaining are Django ORM/DRF stubs issues). --- airavata-django-portal/django_airavata/__init__.py | 2 +- .../django_airavata/apps/api/helpers.py | 15 ++++---- .../django_airavata/apps/api/output_views.py | 11 +++--- .../django_airavata/apps/api/proto_utils.py | 22 +++++++---- .../django_airavata/apps/api/serializers.py | 17 ++++---- .../django_airavata/apps/api/user_storage.py | 45 +++++++++++----------- .../django_airavata/apps/api/view_utils.py | 4 +- .../django_airavata/apps/api/views.py | 15 +++++--- .../django_airavata/apps/auth/backends.py | 2 +- .../django_airavata/apps/auth/middleware.py | 10 +++-- .../apps/auth/tests/test_backends.py | 6 +-- .../apps/auth/tests/test_middleware.py | 4 +- .../django_airavata/apps/auth/views.py | 2 +- .../django_airavata/apps/groups/forms.py | 4 +- .../django_airavata/context_processors.py | 3 +- .../django_airavata/middleware.py | 5 ++- .../django_airavata/proto_compat.py | 18 +++++++-- airavata-django-portal/django_airavata/settings.py | 2 +- airavata-django-portal/django_airavata/types.py | 20 ++++++++++ airavata-django-portal/django_airavata/utils.py | 2 +- .../django_airavata/wagtailapps/base/blocks.py | 2 +- 21 files changed, 127 insertions(+), 84 deletions(-) diff --git a/airavata-django-portal/django_airavata/__init__.py b/airavata-django-portal/django_airavata/__init__.py index 2085355a5..7d927d752 100644 --- a/airavata-django-portal/django_airavata/__init__.py +++ b/airavata-django-portal/django_airavata/__init__.py @@ -3,7 +3,7 @@ from django.conf import settings # Only use PyMySQL as the db driver when in a local dev environment if settings.DEBUG: try: - import pymysql + import pymysql # ty: ignore[unresolved-import] pymysql.install_as_MySQLdb() except ImportError: diff --git a/airavata-django-portal/django_airavata/apps/api/helpers.py b/airavata-django-portal/django_airavata/apps/api/helpers.py index de7eef814..0db5e8421 100644 --- a/airavata-django-portal/django_airavata/apps/api/helpers.py +++ b/airavata-django-portal/django_airavata/apps/api/helpers.py @@ -3,7 +3,6 @@ from typing import Any from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest from django_airavata.proto_compat import ResourcePermissionType @@ -13,7 +12,7 @@ logger = logging.getLogger(__name__) class WorkspacePreferencesHelper: - def get(self, request: HttpRequest) -> models.WorkspacePreferences: + def get(self, request: Any) -> models.WorkspacePreferences: try: workspace_preferences = models.WorkspacePreferences.objects.get(username=request.user.username) self._check(request, workspace_preferences) @@ -22,7 +21,7 @@ class WorkspacePreferencesHelper: workspace_preferences.save() return workspace_preferences - def _create_default(self, request: HttpRequest) -> models.WorkspacePreferences: + def _create_default(self, request: Any) -> models.WorkspacePreferences: workspace_preferences = models.WorkspacePreferences.create(request.user.username) most_recent_project = self._get_most_recent_project(request) workspace_preferences.most_recent_project_id = most_recent_project.projectID @@ -32,7 +31,7 @@ class WorkspacePreferencesHelper: ) return workspace_preferences - def _get_most_recent_project(self, request: HttpRequest) -> Any: + def _get_most_recent_project(self, request: Any) -> Any: "Return most recent writeable project." projects = request.airavata_client.research.get_user_projects(settings.GATEWAY_ID, request.user.username, -1, 0) for project in projects: @@ -40,7 +39,7 @@ class WorkspacePreferencesHelper: return project return None - def _get_first_group_resource_profile(self, request: HttpRequest) -> Any: + def _get_first_group_resource_profile(self, request: Any) -> Any: "Return first accessible group resource profile" group_resource_profiles = request.airavata_client.compute.get_group_resource_list(settings.GATEWAY_ID) @@ -49,7 +48,7 @@ class WorkspacePreferencesHelper: else: return None - def _check(self, request: HttpRequest, prefs: models.WorkspacePreferences) -> None: + def _check(self, request: Any, prefs: models.WorkspacePreferences) -> None: "Validate preference values and update as needed." if not prefs.most_recent_project_id or not self._can_write(request, prefs.most_recent_project_id): most_recent_project = self._get_most_recent_project(request) @@ -72,8 +71,8 @@ class WorkspacePreferencesHelper: prefs.most_recent_group_resource_profile_id = first_grp_id prefs.save() - def _can_write(self, request: HttpRequest, entity_id: str) -> bool: + def _can_write(self, request: Any, entity_id: str) -> bool: return request.airavata_client.sharing.user_has_access(entity_id, ResourcePermissionType.WRITE) - def _can_read(self, request: HttpRequest, entity_id: str) -> bool: + def _can_read(self, request: Any, entity_id: str) -> bool: return request.airavata_client.sharing.user_has_access(entity_id, ResourcePermissionType.READ) diff --git a/airavata-django-portal/django_airavata/apps/api/output_views.py b/airavata-django-portal/django_airavata/apps/api/output_views.py index 46f1325cb..119bffa2b 100644 --- a/airavata-django-portal/django_airavata/apps/api/output_views.py +++ b/airavata-django-portal/django_airavata/apps/api/output_views.py @@ -10,7 +10,6 @@ import nbformat import papermill as pm from django_airavata.apps.api import user_storage from django.conf import settings -from django.http import HttpRequest from nbconvert import HTMLExporter from django_airavata.proto_compat import DataType @@ -28,7 +27,7 @@ class DefaultViewProvider: immediate = False name = "Default" - def generate_data(self, request: HttpRequest, experiment_output: Any, experiment: Any, output_file: Any = None, **kwargs: Any) -> dict[str, Any]: + def generate_data(self, request: Any, experiment_output: Any, experiment: Any, output_file: Any = None, **kwargs: Any) -> dict[str, Any]: return {} @@ -37,7 +36,7 @@ class ParameterizedNotebookViewProvider: name = "Example Parameterized Notebook View" # test_output_file = os.path.join(BASE_DIR, "data", "Gaussian.log") - def generate_data(self, request: HttpRequest, experiment_output: Any, experiment: Any, output_file: Any = None, output_dir: str | None = None) -> dict[str, str]: + def generate_data(self, request: Any, experiment_output: Any, experiment: Any, output_file: Any = None, output_dir: str | None = None) -> dict[str, str]: # use papermill to generate the output notebook output_file_path = os.path.realpath(output_file.name) pm.execute_notebook( @@ -57,7 +56,7 @@ class ParameterizedNotebookViewProvider: DEFAULT_VIEW_PROVIDERS: dict[str, Any] = {"default": DefaultViewProvider()} -def get_output_views(request: HttpRequest, experiment: Any, application_interface: Any = None) -> dict[str, list[dict[str, Any]]]: +def get_output_views(request: Any, experiment: Any, application_interface: Any = None) -> dict[str, list[dict[str, Any]]]: output_views: dict[str, list[dict[str, Any]]] = {} for output in experiment.experimentOutputs: output_views[output.name] = [] @@ -135,7 +134,7 @@ def _get_application_output_view_providers(application_interface: Any, output_na return [] -def generate_data(request: HttpRequest, output_view_provider_id: str, experiment_output_name: str, experiment_id: str, test_mode: bool = False, **kwargs: Any) -> dict[str, Any]: +def generate_data(request: Any, output_view_provider_id: str, experiment_output_name: str, experiment_id: str, test_mode: bool = False, **kwargs: Any) -> dict[str, Any]: output_view_provider = _get_output_view_provider(output_view_provider_id) # TODO if output_view_provider is None, return 404 experiment = request.airavata_client.research.get_experiment(experiment_id) @@ -148,7 +147,7 @@ def generate_data(request: HttpRequest, output_view_provider_id: str, experiment return _generate_data(request, output_view_provider, experiment_output, experiment, test_mode=test_mode, **kwargs) -def _generate_data(request: HttpRequest, output_view_provider: Any, experiment_output: Any, experiment: Any, test_mode: bool = False, **kwargs: Any) -> dict[str, Any]: +def _generate_data(request: Any, output_view_provider: Any, experiment_output: Any, experiment: Any, test_mode: bool = False, **kwargs: Any) -> dict[str, Any]: output_files: list[Any] = [] # test_mode can only be used in DEBUG=True mode if test_mode and settings.DEBUG: diff --git a/airavata-django-portal/django_airavata/apps/api/proto_utils.py b/airavata-django-portal/django_airavata/apps/api/proto_utils.py index add87d795..dc0952ec5 100644 --- a/airavata-django-portal/django_airavata/apps/api/proto_utils.py +++ b/airavata-django-portal/django_airavata/apps/api/proto_utils.py @@ -67,12 +67,12 @@ class UTCPosixTimestampDateTimeField(DateTimeField): self.initial = self.initial_value self.required = False - def to_representation(self, obj): - dt = datetime.datetime.fromtimestamp(obj / 1000, datetime.UTC) + def to_representation(self, value): # type: ignore[override] + dt = datetime.datetime.fromtimestamp(value / 1000, datetime.UTC) return super().to_representation(dt) - def to_internal_value(self, data): - dt = super().to_internal_value(data) + def to_internal_value(self, value): # type: ignore[override] + dt = super().to_internal_value(value) return int(dt.timestamp() * 1000) def initial_value(self): @@ -87,10 +87,10 @@ class ThriftEnumField(Field): super().__init__(*args, **kwargs) self.enumClass = enumClass - def to_representation(self, obj): - if obj is None: + def to_representation(self, value): # type: ignore[override] + if value is None: return None - return obj.name + return value.name def to_internal_value(self, data): if self.allow_null and data is None: @@ -208,6 +208,9 @@ def process_field(field, enable_date_time_conversion, required=False, read_only= kwargs["allow_null"] = allow_null if field_class == CharField: kwargs["allow_blank"] = allow_null + if field_class == DecimalField: + kwargs["max_digits"] = 65 + kwargs["decimal_places"] = 30 thrift_model_class = mapping[field[1]] if ( @@ -242,6 +245,9 @@ def process_list_field(field): return ThriftEnumField(item_type_info) if item_ttype in mapping: - return mapping[item_ttype]() + field_cls = mapping[item_ttype] + if field_cls == DecimalField: + return field_cls(max_digits=65, decimal_places=30) + return field_cls() elif item_ttype == TTYPE_STRUCT: return create_serializer(item_type_info[0]) diff --git a/airavata-django-portal/django_airavata/apps/api/serializers.py b/airavata-django-portal/django_airavata/apps/api/serializers.py index a016d5832..d7906f4ef 100644 --- a/airavata-django-portal/django_airavata/apps/api/serializers.py +++ b/airavata-django-portal/django_airavata/apps/api/serializers.py @@ -90,13 +90,13 @@ class UTCPosixTimestampDateTimeField(serializers.DateTimeField): self.initial = self.initial_value self.required = False - def to_representation(self, obj: int) -> str: + def to_representation(self, value): # type: ignore[override] # Create datetime instance from milliseconds that is aware of timezon - dt = datetime.datetime.fromtimestamp(obj / 1000, datetime.UTC) + dt = datetime.datetime.fromtimestamp(value / 1000, datetime.UTC) return super().to_representation(dt) - def to_internal_value(self, data: str) -> int: - dt = super().to_internal_value(data) + def to_internal_value(self, value): # type: ignore[override] + dt = super().to_internal_value(value) return int(dt.timestamp() * 1000) def initial_value(self) -> str: @@ -124,6 +124,7 @@ class StoredJSONField(serializers.JSONField): return json.dumps(data) except (TypeError, ValueError): self.fail("invalid") + raise # unreachable, but satisfies type checker class OrderedListField(serializers.ListField): @@ -131,8 +132,8 @@ class OrderedListField(serializers.ListField): self.order_by = kwargs.pop("order_by", None) super().__init__(*args, **kwargs) - def to_representation(self, instance: list[Any]) -> list[dict[str, Any]] | None: - rep = super().to_representation(instance) + def to_representation(self, data): # type: ignore[override] + rep = super().to_representation(data) if rep is not None: rep.sort(key=lambda item: item[self.order_by]) return rep @@ -281,7 +282,7 @@ class ApplicationModuleSerializer(proto_utils.create_serializer_class(Applicatio class EnumChoiceField(serializers.ChoiceField): - def __init__(self, enum_class: type, **kwargs: Any) -> None: + def __init__(self, enum_class: Any, **kwargs: Any) -> None: self.enum_class = enum_class kwargs["choices"] = [(member.name, member.name) for member in enum_class] super().__init__(**kwargs) @@ -1804,7 +1805,7 @@ class UserHasWriteAccessToPathSerializer(serializers.Serializer): if path != Path(""): # get parent directory listing and use that to figure out if # there is write access to this directory - directories, _ = user_storage.listdir(request, path.parent) + directories, _ = user_storage.listdir(request, str(path.parent)) for d in directories: if Path(d["path"]) == path: return d.get("userHasWriteAccess", False) diff --git a/airavata-django-portal/django_airavata/apps/api/user_storage.py b/airavata-django-portal/django_airavata/apps/api/user_storage.py index aed80ba33..b8d244443 100644 --- a/airavata-django-portal/django_airavata/apps/api/user_storage.py +++ b/airavata-django-portal/django_airavata/apps/api/user_storage.py @@ -11,7 +11,6 @@ import os from typing import Any, BinaryIO from django.conf import settings -from django.http import HttpRequest log = logging.getLogger(__name__) @@ -40,7 +39,7 @@ def _get_replica_storage_resource_id(data_product: Any) -> str | None: # File existence / metadata # --------------------------------------------------------------------------- -def exists(request: HttpRequest, data_product: Any) -> bool: +def exists(request: Any, data_product: Any) -> bool: """Check whether the file backing *data_product* exists in user storage.""" path = _get_replica_filepath(data_product) if not path: @@ -51,14 +50,14 @@ def exists(request: HttpRequest, data_product: Any) -> bool: return False -def dir_exists(request: HttpRequest, path: str, experiment_id: str | None = None) -> bool: +def dir_exists(request: Any, path: str, experiment_id: str | None = None) -> bool: """Check whether *path* exists as a directory in user storage.""" if experiment_id: return experiment_dir_exists(request, experiment_id, path) return request.airavata_client.storage.dir_exists(path) -def experiment_dir_exists(request: HttpRequest, experiment_id: str, path: str = "") -> bool: +def experiment_dir_exists(request: Any, experiment_id: str, path: str = "") -> bool: """Check whether the experiment output directory exists.""" try: request.airavata_client.storage.list_experiment_dir(experiment_id, path) @@ -67,7 +66,7 @@ def experiment_dir_exists(request: HttpRequest, experiment_id: str, path: str = return False -def is_input_file(request: HttpRequest, data_product: Any) -> bool: +def is_input_file(request: Any, data_product: Any) -> bool: """Return True if the data product's path is under the inputs directory.""" path = _get_replica_filepath(data_product) if not path: @@ -81,7 +80,7 @@ def is_input_file(request: HttpRequest, data_product: Any) -> bool: # File / directory listing # --------------------------------------------------------------------------- -def listdir(request: HttpRequest, path: str, experiment_id: str | None = None) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: +def listdir(request: Any, path: str, experiment_id: str | None = None) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: """List the contents of *path*, returning (directories, files) dicts.""" if experiment_id: return list_experiment_dir(request, experiment_id, path) @@ -91,7 +90,7 @@ def listdir(request: HttpRequest, path: str, experiment_id: str | None = None) - return directories, files -def list_experiment_dir(request: HttpRequest, experiment_id: str, path: str = "") -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: +def list_experiment_dir(request: Any, experiment_id: str, path: str = "") -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: """List the experiment output directory.""" resp = request.airavata_client.storage.list_experiment_dir(experiment_id, path) directories = _metadata_list_to_dicts(resp.directories) @@ -120,9 +119,10 @@ def _metadata_list_to_dicts(items: Any) -> list[dict[str, Any]]: # File open / download # --------------------------------------------------------------------------- -def open_file(request: HttpRequest, data_product: Any) -> io.BytesIO: +def open_file(request: Any, data_product: Any) -> io.BytesIO: """Download the file for *data_product* and return a file-like object.""" path = _get_replica_filepath(data_product) + assert path is not None, "data_product has no replica file path" resp = request.airavata_client.storage.download_file(path) f = io.BytesIO(resp.content) f.name = resp.name or os.path.basename(path) @@ -133,7 +133,7 @@ def open_file(request: HttpRequest, data_product: Any) -> io.BytesIO: # File upload / save # --------------------------------------------------------------------------- -def save_input_file(request: HttpRequest, input_file: BinaryIO, name: str | None = None, content_type: str = "") -> Any: +def save_input_file(request: Any, input_file: BinaryIO, name: str | None = None, content_type: str = "") -> Any: """Upload *input_file* to the user's input files directory. Returns a DataProductModel proto. @@ -151,7 +151,7 @@ def save_input_file(request: HttpRequest, input_file: BinaryIO, name: str | None return request.airavata_client.research.get_data_product(resp.uri) -def save(request: HttpRequest, path: str, file_obj: BinaryIO, name: str | None = None, content_type: str = "", experiment_id: str | None = None) -> Any: +def save(request: Any, path: str, file_obj: BinaryIO, name: str | None = None, content_type: str = "", experiment_id: str | None = None) -> Any: """Upload *file_obj* to *path* in user storage. Returns a DataProductModel proto. @@ -171,9 +171,10 @@ def save(request: HttpRequest, path: str, file_obj: BinaryIO, name: str | None = # File content update # --------------------------------------------------------------------------- -def update_data_product_content(request: HttpRequest, data_product: Any, fileContentText: str) -> None: +def update_data_product_content(request: Any, data_product: Any, fileContentText: str) -> None: """Replace the content of the file backing *data_product* with *fileContentText*.""" path = _get_replica_filepath(data_product) + assert path is not None, "data_product has no replica file path" name = os.path.basename(path) request.airavata_client.storage.upload_file( path=os.path.dirname(path), @@ -182,7 +183,7 @@ def update_data_product_content(request: HttpRequest, data_product: Any, fileCon ) -def update_file_content(request: HttpRequest, path: str, fileContentText: str) -> None: +def update_file_content(request: Any, path: str, fileContentText: str) -> None: """Replace the content of the file at *path* with *fileContentText*.""" name = os.path.basename(path) request.airavata_client.storage.upload_file( @@ -196,30 +197,30 @@ def update_file_content(request: HttpRequest, path: str, fileContentText: str) - # File / directory creation and deletion # --------------------------------------------------------------------------- -def create_user_dir(request: HttpRequest, path: str, experiment_id: str | None = None) -> tuple[None, str]: +def create_user_dir(request: Any, path: str, experiment_id: str | None = None) -> tuple[None, str]: """Create a directory at *path*. Returns (storage_resource_id, created_path).""" resp = request.airavata_client.storage.create_dir(path) return None, resp.created_path -def create_symlink(request: HttpRequest, source_path: str, dest_path: str) -> None: +def create_symlink(request: Any, source_path: str, dest_path: str) -> None: """Create a symlink from *source_path* to *dest_path*.""" request.airavata_client.storage.create_symlink(source_path, dest_path) -def delete(request: HttpRequest, data_product: Any) -> None: +def delete(request: Any, data_product: Any) -> None: """Delete the file backing *data_product*.""" path = _get_replica_filepath(data_product) if path: request.airavata_client.storage.delete_file(path) -def delete_user_file(request: HttpRequest, path: str, experiment_id: str | None = None) -> None: +def delete_user_file(request: Any, path: str, experiment_id: str | None = None) -> None: """Delete a user file at *path*.""" request.airavata_client.storage.delete_file(path) -def delete_dir(request: HttpRequest, path: str, experiment_id: str | None = None) -> None: +def delete_dir(request: Any, path: str, experiment_id: str | None = None) -> None: """Delete a directory at *path*.""" request.airavata_client.storage.delete_dir(path) @@ -228,7 +229,7 @@ def delete_dir(request: HttpRequest, path: str, experiment_id: str | None = None # File metadata # --------------------------------------------------------------------------- -def get_file_metadata(request: HttpRequest, path: str, experiment_id: str | None = None) -> dict[str, Any]: +def get_file_metadata(request: Any, path: str, experiment_id: str | None = None) -> dict[str, Any]: """Get metadata for the file at *path*. Returns a dict.""" resp = request.airavata_client.storage.get_file_metadata(path) return { @@ -243,7 +244,7 @@ def get_file_metadata(request: HttpRequest, path: str, experiment_id: str | None } -def get_data_product_metadata(request: HttpRequest, data_product: Any = None, data_product_uri: str | None = None) -> dict[str, Any]: +def get_data_product_metadata(request: Any, data_product: Any = None, data_product_uri: str | None = None) -> dict[str, Any]: """Get metadata for a data product. Returns a dict with path, size, etc.""" if data_product is None and data_product_uri: data_product = request.airavata_client.research.get_data_product(data_product_uri) @@ -270,14 +271,14 @@ def get_data_product_metadata(request: HttpRequest, data_product: Any = None, da # Download URL helpers # --------------------------------------------------------------------------- -def get_download_url(request: HttpRequest, data_product_uri: str | None = None) -> str: +def get_download_url(request: Any, data_product_uri: str | None = None) -> str: """Return a URL to download the file for *data_product_uri*.""" from django.urls import reverse from urllib.parse import quote - return reverse("django_airavata_api:download_file") + f"?data-product-uri={quote(data_product_uri)}" + return reverse("django_airavata_api:download_file") + f"?data-product-uri={quote(data_product_uri or '')}" -def get_lazy_download_url(request: HttpRequest, data_product: Any = None, data_product_uri: str | None = None) -> str | None: +def get_lazy_download_url(request: Any, data_product: Any = None, data_product_uri: str | None = None) -> str | None: """Return a download URL. Accepts either a data_product or data_product_uri.""" if data_product_uri: return get_download_url(request, data_product_uri=data_product_uri) diff --git a/airavata-django-portal/django_airavata/apps/api/view_utils.py b/airavata-django-portal/django_airavata/apps/api/view_utils.py index a1bc917a1..9a67c8763 100644 --- a/airavata-django-portal/django_airavata/apps/api/view_utils.py +++ b/airavata-django-portal/django_airavata/apps/api/view_utils.py @@ -6,7 +6,7 @@ from datetime import datetime from pathlib import Path from typing import Any -import pytz +import pytz # ty: ignore[unresolved-import] from django_airavata.apps.api import user_storage from django.conf import settings from django.http import Http404 @@ -170,7 +170,7 @@ class APIResultPagination(pagination.LimitOffsetPagination): return super().get_limit(request) def get_paginated_response(self, data: list[Any]) -> Response: - has_next_link = len(data) >= self.limit + has_next_link = self.limit is not None and len(data) >= self.limit return Response( OrderedDict( [ diff --git a/airavata-django-portal/django_airavata/apps/api/views.py b/airavata-django-portal/django_airavata/apps/api/views.py index d5e421aa8..601a3227e 100644 --- a/airavata-django-portal/django_airavata/apps/api/views.py +++ b/airavata-django-portal/django_airavata/apps/api/views.py @@ -108,8 +108,8 @@ class GroupViewSet(APIBackedViewSet): sharing_client.remove_group_admins(group.id, group._removed_admins) sharing_client.update_group(group) - def perform_destroy(self, group: Any) -> None: - self.request.airavata_client.sharing.delete_group(group.id, group.ownerId) + def perform_destroy(self, instance: Any) -> None: # type: ignore[override] + self.request.airavata_client.sharing.delete_group(instance.id, instance.ownerId) def _send_users_added_to_group(self, internal_user_ids: set[str], group: Any) -> None: for internal_user_id in internal_user_ids: @@ -1185,6 +1185,7 @@ class SharedEntityViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, Ge @action(methods=["get"], detail=True) def all(self, request: Request, entity_id: str | None = None) -> Response: """Load direct plus indirectly (inherited) shared permissions.""" + assert entity_id is not None, "entity_id is required" users = {} # Load accessible users in order of permission precedence: users that # have WRITE permission should also have READ @@ -1388,7 +1389,7 @@ class UserStoragePathView(APIView): experiment_id = request.query_params.get("experiment-id") return self._create_response(request, path, experiment_id=experiment_id) - def post(self, request: Request, path: str = "/", format: str | None = None) -> Response: + def post(self, request: Request, path: str = "/", format: str | None = None, file_name: str | None = None) -> Response: path = request.data.get("path", path) experiment_id = request.data.get("experiment-id") if not user_storage.dir_exists(request, path, experiment_id=experiment_id): @@ -1454,7 +1455,7 @@ class UserStoragePathView(APIView): def _create_response(self, request: Request, path: str, uploaded: Any = None, experiment_id: str | None = None) -> Response: if user_storage.dir_exists(request, path, experiment_id=experiment_id): directories, files = user_storage.listdir(request, path, experiment_id=experiment_id) - data = {"isDir": True, "directories": directories, "files": files} + data: dict[str, Any] = {"isDir": True, "directories": directories, "files": files} if uploaded is not None: data["uploaded"] = uploaded data["parts"] = self._split_path(path) @@ -1463,7 +1464,7 @@ class UserStoragePathView(APIView): return Response(serializer.data) else: file = user_storage.get_file_metadata(request, path, experiment_id=experiment_id) - data = {"isDir": False, "directories": [], "files": [file]} + data: dict[str, Any] = {"isDir": False, "directories": [], "files": [file]} if uploaded is not None: data["uploaded"] = uploaded data["parts"] = self._split_path(path) @@ -1484,6 +1485,7 @@ class ExperimentStoragePathView(APIView): serializer_class = serializers.ExperimentStoragePathSerializer def get(self, request: Request, experiment_id: str | None = None, path: str = "", format: str | None = None) -> Response: + assert experiment_id is not None, "experiment_id is required" return self._create_response(request, experiment_id, path) def _create_response(self, request: Request, experiment_id: str, path: str) -> Response: @@ -1494,7 +1496,7 @@ class ExperimentStoragePathView(APIView): d["experiment_id"] = experiment_id return d - data = {"isDir": True, "directories": map(add_expid, directories), "files": map(add_expid, files)} + data: dict[str, Any] = {"isDir": True, "directories": map(add_expid, directories), "files": map(add_expid, files)} data["parts"] = self._split_path(path) serializer = self.serializer_class(data, context={"request": request}) return Response(serializer.data) @@ -1617,6 +1619,7 @@ class IAMUserViewSet( @action(methods=["post"], detail=True) def enable(self, request: Request, user_id: str | None = None) -> Response: + assert user_id is not None, "user_id is required" iam_admin_client.enable_user(user_id) instance = self.get_instance(user_id) serializer = self.serializer_class(instance=instance, context={"request": request}) diff --git a/airavata-django-portal/django_airavata/apps/auth/backends.py b/airavata-django-portal/django_airavata/apps/auth/backends.py index e9b0fc207..2230a04fb 100644 --- a/airavata-django-portal/django_airavata/apps/auth/backends.py +++ b/airavata-django-portal/django_airavata/apps/auth/backends.py @@ -74,7 +74,7 @@ class KeycloakBackend: # authz_token_middleware has already run, so must manually add # the `request.authz_token` attribute if user is not None: - request.authz_token = get_authz_token(request, user=user, access_token=access_token) + request.authz_token = get_authz_token(request, user=user, access_token=access_token) # ty: ignore[invalid-assignment] return user except Exception as e: logger.warning("login failed", exc_info=e) diff --git a/airavata-django-portal/django_airavata/apps/auth/middleware.py b/airavata-django-portal/django_airavata/apps/auth/middleware.py index 195090cf3..0b702636c 100644 --- a/airavata-django-portal/django_airavata/apps/auth/middleware.py +++ b/airavata-django-portal/django_airavata/apps/auth/middleware.py @@ -11,6 +11,8 @@ from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect from django.urls import reverse +from django_airavata.types import AiravataRequest + from . import utils log = logging.getLogger(__name__) @@ -19,7 +21,7 @@ log = logging.getLogger(__name__) def authz_token_middleware(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable[[HttpRequest], HttpResponse]: """Automatically add the 'authz_token' to the request.""" - def middleware(request: HttpRequest) -> HttpResponse: + def middleware(request: Any) -> HttpResponse: authz_token = None if request.user.is_authenticated: @@ -36,7 +38,7 @@ def authz_token_middleware(get_response: Callable[[HttpRequest], HttpResponse]) return middleware -def set_admin_group_attributes(request: HttpRequest, gateway_groups: Any = None) -> None: +def set_admin_group_attributes(request: Any, gateway_groups: Any = None) -> None: """Set is_gateway_admin and is_read_only_gateway_admin request attrs.""" if gateway_groups is None: gateway_groups = request.airavata_client.iam.get_gateway_groups() @@ -58,7 +60,7 @@ def set_admin_group_attributes(request: HttpRequest, gateway_groups: Any = None) def gateway_groups_middleware(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable[[HttpRequest], HttpResponse]: """Add 'is_gateway_admin' and 'is_read_only_gateway_admin' to request.""" - def middleware(request: HttpRequest) -> HttpResponse: + def middleware(request: Any) -> HttpResponse: request.is_gateway_admin = False request.is_read_only_gateway_admin = False @@ -99,7 +101,7 @@ def gateway_groups_middleware(get_response: Callable[[HttpRequest], HttpResponse def user_profile_completeness_check(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable[[HttpRequest], HttpResponse]: """Check if user profile is complete and if not, redirect to user profile editor.""" - def middleware(request: HttpRequest) -> HttpResponse: + def middleware(request: Any) -> HttpResponse: if not request.user.is_authenticated: return get_response(request) diff --git a/airavata-django-portal/django_airavata/apps/auth/tests/test_backends.py b/airavata-django-portal/django_airavata/apps/auth/tests/test_backends.py index 656672bdc..dbe04f6ee 100644 --- a/airavata-django-portal/django_airavata/apps/auth/tests/test_backends.py +++ b/airavata-django-portal/django_airavata/apps/auth/tests/test_backends.py @@ -49,7 +49,7 @@ class KeycloakBackendTestCase(TestCase): # Mock out request for redirect flow, and OAuth2Session: token and userinfo request = self.factory.get("/callback?code=abc123", secure=True) request.user = AnonymousUser() - request.session = { + request.session = { # ty: ignore[invalid-assignment] "OAUTH2_STATE": "state", "OAUTH2_REDIRECT_URI": "redirect-uri", } @@ -94,7 +94,7 @@ class KeycloakBackendTestCase(TestCase): # Mock out request for redirect flow, and OAuth2Session: token and userinfo request = self.factory.get("/callback?code=abc123", secure=True) request.user = AnonymousUser() - request.session = { + request.session = { # ty: ignore[invalid-assignment] "OAUTH2_STATE": "state", "OAUTH2_REDIRECT_URI": "redirect-uri", } @@ -141,7 +141,7 @@ class KeycloakBackendTestCase(TestCase): # Mock out request for redirect flow, and OAuth2Session: token and userinfo request = self.factory.get("/callback?code=abc123", secure=True) request.user = AnonymousUser() - request.session = { + request.session = { # ty: ignore[invalid-assignment] "OAUTH2_STATE": "state", "OAUTH2_REDIRECT_URI": "redirect-uri", } diff --git a/airavata-django-portal/django_airavata/apps/auth/tests/test_middleware.py b/airavata-django-portal/django_airavata/apps/auth/tests/test_middleware.py index ee5ff31b9..b591b6945 100644 --- a/airavata-django-portal/django_airavata/apps/auth/tests/test_middleware.py +++ b/airavata-django-portal/django_airavata/apps/auth/tests/test_middleware.py @@ -13,7 +13,7 @@ from django_airavata.apps.auth.middleware import user_profile_completeness_check class UserProfileCompletenessCheckTestCase(TestCase): def setUp(self): User = get_user_model() - self.user: User = User.objects.create_user("testuser") + self.user = User.objects.create_user("testuser") self.user_profile: models.UserProfile = models.UserProfile.objects.create(user=self.user) self.factory = RequestFactory() @@ -99,7 +99,7 @@ class UserProfileCompletenessCheckTestCase(TestCase): """Test user profile is complete, ext user prof is invalid, but user is gateway admin.""" request = self.factory.get(reverse("django_airavata_workspace:dashboard"), HTTP_ACCEPT=["text/html"]) request.user = self.user - request.is_gateway_admin = True + request.is_gateway_admin = True # ty: ignore[invalid-assignment] self.user.first_name = "Admin" self.user.last_name = "User" self.user.email = "[email protected]" diff --git a/airavata-django-portal/django_airavata/apps/auth/views.py b/airavata-django-portal/django_airavata/apps/auth/views.py index a0d202011..a9278069f 100644 --- a/airavata-django-portal/django_airavata/apps/auth/views.py +++ b/airavata-django-portal/django_airavata/apps/auth/views.py @@ -209,7 +209,7 @@ def create_account(request): ) return redirect(reverse("django_airavata_auth:create_account")) except Exception as e: - logger.exception("Failed to create account for user", exc_info=e, extra={"request", request}) + logger.exception("Failed to create account for user", exc_info=e, extra={"request": request}) form.add_error(None, ValidationError(e.message)) else: form = forms.CreateAccountForm(initial=request.GET) diff --git a/airavata-django-portal/django_airavata/apps/groups/forms.py b/airavata-django-portal/django_airavata/apps/groups/forms.py index a880ca9ba..0251720e3 100755 --- a/airavata-django-portal/django_airavata/apps/groups/forms.py +++ b/airavata-django-portal/django_airavata/apps/groups/forms.py @@ -41,10 +41,10 @@ class CreateForm(forms.Form): class AddForm(forms.Form): def __init__(self, data=None, user_choices=None): super().__init__(data=data) - self.fields["users"] = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=user_choices) + self.fields["users"] = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=user_choices or []) class RemoveForm(forms.Form): def __init__(self, data=None, user_choices=None): super().__init__(data=data) - self.fields["members"] = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=user_choices) + self.fields["members"] = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=user_choices or []) diff --git a/airavata-django-portal/django_airavata/context_processors.py b/airavata-django-portal/django_airavata/context_processors.py index fe0077520..7720a5a38 100644 --- a/airavata-django-portal/django_airavata/context_processors.py +++ b/airavata-django-portal/django_airavata/context_processors.py @@ -12,12 +12,13 @@ from django.http import HttpRequest from django.urls import reverse from django_airavata.app_config import AiravataAppConfig +from django_airavata.types import AiravataRequest from django_airavata.apps.api.models import User_Notifications logger = logging.getLogger(__name__) -def get_notifications(request: HttpRequest) -> dict[str, Any]: +def get_notifications(request: AiravataRequest) -> dict[str, Any]: if request.user.is_authenticated and hasattr(request, "airavata_client"): unread_notifications = 0 try: diff --git a/airavata-django-portal/django_airavata/middleware.py b/airavata-django-portal/django_airavata/middleware.py index c908ca184..3e145609a 100644 --- a/airavata-django-portal/django_airavata/middleware.py +++ b/airavata-django-portal/django_airavata/middleware.py @@ -1,5 +1,6 @@ import logging from collections.abc import Callable +from typing import Any from django.conf import settings from django.http import HttpRequest, HttpResponse @@ -14,7 +15,7 @@ class AiravataClientMiddleware: def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: self.get_response = get_response - def __call__(self, request: HttpRequest) -> HttpResponse: + def __call__(self, request: Any) -> HttpResponse: access_token = _get_access_token(request) gateway_id = settings.GATEWAY_ID request.airavata_client = create_airavata_client(access_token, gateway_id) @@ -45,5 +46,5 @@ class AiravataClientMiddleware: def _get_access_token(request: HttpRequest) -> str: """Extract access token from request auth or session.""" if hasattr(request, "auth") and request.auth is not None: - return request.auth + return str(request.auth) return request.session.get("ACCESS_TOKEN", "") diff --git a/airavata-django-portal/django_airavata/proto_compat.py b/airavata-django-portal/django_airavata/proto_compat.py index 7f3062e98..614cd97b7 100644 --- a/airavata-django-portal/django_airavata/proto_compat.py +++ b/airavata-django-portal/django_airavata/proto_compat.py @@ -9,7 +9,10 @@ and read via attribute access), simple Python classes with __init__(**kwargs) suffice since the SDK's gRPC facade returns these objects already. """ +from __future__ import annotations + import enum +from typing import Any # --------------------------------------------------------------------------- # Enums @@ -63,6 +66,7 @@ class ResourcePermissionType(enum.IntEnum): WRITE = 0 READ = 1 MANAGE_SHARING = 2 + OWNER = 3 class ResourceType(enum.IntEnum): @@ -161,7 +165,8 @@ class SetEnvPaths(_ThriftLikeBase): class ApplicationInterfaceDescription(_ThriftLikeBase): - pass + applicationInputs: list[InputDataObjectType] + applicationOutputs: list[OutputDataObjectType] # -- App Catalog: compute resource -- @@ -214,7 +219,9 @@ class ComputeResourceReservation(_ThriftLikeBase): class GroupComputeResourcePreference(_ThriftLikeBase): - pass + resourceType: ResourceType | None + specificPreferences: Any + computeResourceId: str | None class GroupResourceProfile(_ThriftLikeBase): @@ -230,7 +237,8 @@ class BatchQueueResourcePolicy(_ThriftLikeBase): class EnvironmentSpecificPreferences(_ThriftLikeBase): - pass + slurm: Any + aws: Any class SlurmComputeResourcePreference(_ThriftLikeBase): @@ -315,7 +323,9 @@ class ExperimentSummaryModel(_ThriftLikeBase): class GroupModel(_ThriftLikeBase): - pass + name: str | None + id: str | None + ownerId: str | None # -- Job -- diff --git a/airavata-django-portal/django_airavata/settings.py b/airavata-django-portal/django_airavata/settings.py index 0b545ec1b..d2de9a8fa 100644 --- a/airavata-django-portal/django_airavata/settings.py +++ b/airavata-django-portal/django_airavata/settings.py @@ -568,7 +568,7 @@ WAGTAIL_CODE_BLOCK_LANGUAGES = ( # Allow all settings to be overridden by settings_local.py file try: - from django_airavata.settings_local import * # noqa + from django_airavata.settings_local import * # ty: ignore[unresolved-import] # noqa except ImportError: pass diff --git a/airavata-django-portal/django_airavata/types.py b/airavata-django-portal/django_airavata/types.py new file mode 100644 index 000000000..592ffae0a --- /dev/null +++ b/airavata-django-portal/django_airavata/types.py @@ -0,0 +1,20 @@ +"""Type definitions for the Airavata Django Portal.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from django.http import HttpRequest + +if TYPE_CHECKING: + from airavata_sdk.client import AiravataClient # ty: ignore[unresolved-import] + + +class AiravataRequest(HttpRequest): + """Extended HttpRequest with Airavata client attached by middleware.""" + + airavata_client: AiravataClient + authz_token: dict[str, Any] | None + is_gateway_admin: bool + is_read_only_gateway_admin: bool + active_nav_item: str diff --git a/airavata-django-portal/django_airavata/utils.py b/airavata-django-portal/django_airavata/utils.py index 23b7bac21..b5fa06695 100644 --- a/airavata-django-portal/django_airavata/utils.py +++ b/airavata-django-portal/django_airavata/utils.py @@ -1,6 +1,6 @@ import logging -from airavata_sdk import AiravataClient +from airavata_sdk import AiravataClient # ty: ignore[unresolved-import] from django.conf import settings log = logging.getLogger(__name__) diff --git a/airavata-django-portal/django_airavata/wagtailapps/base/blocks.py b/airavata-django-portal/django_airavata/wagtailapps/base/blocks.py index 45dbfe01c..fecf017e6 100644 --- a/airavata-django-portal/django_airavata/wagtailapps/base/blocks.py +++ b/airavata-django-portal/django_airavata/wagtailapps/base/blocks.py @@ -13,7 +13,7 @@ from wagtail.blocks import ( from wagtail.documents.blocks import DocumentChooserBlock from wagtail.embeds.blocks import EmbedBlock from wagtail.images.blocks import ImageChooserBlock -from wagtailcodeblock.blocks import CodeBlock +from wagtailcodeblock.blocks import CodeBlock # ty: ignore[unresolved-import] class ImageBlock(StructBlock):
