You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/04/08 07:02:31 UTC
(airavata-portals) 21/21: fix: resolve ty type errors, add AiravataRequest type stub
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 <ya...@icloud.com>
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 = "admin@gateway.edu"
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):