diff --git a/client/src/components/History/CurrentHistory/HistoryCounter.vue b/client/src/components/History/CurrentHistory/HistoryCounter.vue
index 30ef239cf236..0c41424f55f0 100644
--- a/client/src/components/History/CurrentHistory/HistoryCounter.vue
+++ b/client/src/components/History/CurrentHistory/HistoryCounter.vue
@@ -12,19 +12,25 @@
{{ historySize | niceFileSize }}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
{
let axiosMock;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
- axiosMock.onGet("/api/object_store?selectable=true").reply(200, OBJECT_STORES);
});
afterEach(async () => {
@@ -44,10 +44,10 @@ describe("SelectPreferredStore.vue", () => {
it("updates object store to default on selection null", async () => {
const wrapper = mountComponent();
await flushPromises();
- const els = wrapper.findAll(ROOT_COMPONENT.preferences.object_store_selection.option_buttons.selector);
+ const els = wrapper.findAll(PREFERENCES.object_store_selection.option_buttons.selector);
expect(els.length).toBe(3);
const galaxyDefaultOption = wrapper.find(
- ROOT_COMPONENT.preferences.object_store_selection.option_button({ object_store_id: "__null__" }).selector
+ PREFERENCES.object_store_selection.option_button({ object_store_id: "__null__" }).selector
);
expect(galaxyDefaultOption.exists()).toBeTruthy();
axiosMock
diff --git a/client/src/components/History/CurrentHistory/SelectPreferredStore.vue b/client/src/components/History/CurrentHistory/SelectPreferredStore.vue
index 6cf49f4f0801..0fd3195918f4 100644
--- a/client/src/components/History/CurrentHistory/SelectPreferredStore.vue
+++ b/client/src/components/History/CurrentHistory/SelectPreferredStore.vue
@@ -35,27 +35,27 @@ export default {
selectedObjectStoreId: selectedObjectStoreId,
newDatasetsDescription: "New dataset outputs from tools and workflows executed in this history",
popoverPlacement: "left",
- galaxySelectionDefalutTitle: "Use Galaxy Defaults",
- galaxySelectionDefalutDescription:
+ galaxySelectionDefaultTitle: "Use Galaxy Defaults",
+ galaxySelectionDefaultDescription:
"Selecting this will reset Galaxy to default behaviors configured by your Galaxy administrator.",
- userSelectionDefalutTitle: "Use Your User Preference Defaults",
- userSelectionDefalutDescription:
+ userSelectionDefaultTitle: "Use Your User Preference Defaults",
+ userSelectionDefaultDescription:
"Selecting this will cause the history to not set a default and to fallback to your user preference defined default.",
};
},
computed: {
defaultOptionTitle() {
if (this.userPreferredObjectStoreId) {
- return this.userSelectionDefalutTitle;
+ return this.userSelectionDefaultTitle;
} else {
- return this.galaxySelectionDefalutTitle;
+ return this.galaxySelectionDefaultTitle;
}
},
defaultOptionDescription() {
if (this.userPreferredObjectStoreId) {
- return this.userSelectionDefalutDescription;
+ return this.userSelectionDefaultDescription;
} else {
- return this.galaxySelectionDefalutDescription;
+ return this.galaxySelectionDefaultDescription;
}
},
},
diff --git a/client/src/components/ObjectStore/SelectObjectStore.vue b/client/src/components/ObjectStore/SelectObjectStore.vue
index 2d3096f4b04c..0ce3cb152923 100644
--- a/client/src/components/ObjectStore/SelectObjectStore.vue
+++ b/client/src/components/ObjectStore/SelectObjectStore.vue
@@ -1,3 +1,89 @@
+
+
@@ -55,97 +141,3 @@
-
-
diff --git a/client/src/components/ObjectStore/mockServices.ts b/client/src/components/ObjectStore/mockServices.ts
new file mode 100644
index 000000000000..bd9e0f80c4e8
--- /dev/null
+++ b/client/src/components/ObjectStore/mockServices.ts
@@ -0,0 +1,12 @@
+import { getSelectableObjectStores } from "./services";
+jest.mock("./services");
+
+const OBJECT_STORES = [
+ { object_store_id: "object_store_1", badges: [], quota: { enabled: false }, private: false },
+ { object_store_id: "object_store_2", badges: [], quota: { enabled: false }, private: false },
+];
+
+export function setupSelectableMock() {
+ const mockGetObjectStores = getSelectableObjectStores as jest.MockedFunction;
+ mockGetObjectStores.mockResolvedValue(OBJECT_STORES);
+}
diff --git a/client/src/components/ObjectStore/services.ts b/client/src/components/ObjectStore/services.ts
new file mode 100644
index 000000000000..418526f17818
--- /dev/null
+++ b/client/src/components/ObjectStore/services.ts
@@ -0,0 +1,8 @@
+import { fetcher } from "@/schema/fetcher";
+
+const getObjectStores = fetcher.path("/api/object_store").method("get").create();
+
+export async function getSelectableObjectStores() {
+ const { data } = await getObjectStores({ selectable: true });
+ return data;
+}
diff --git a/client/src/components/Tool/ToolCard.vue b/client/src/components/Tool/ToolCard.vue
index f877b14af41c..72100d6c6e84 100644
--- a/client/src/components/Tool/ToolCard.vue
+++ b/client/src/components/Tool/ToolCard.vue
@@ -130,7 +130,7 @@ function onUpdatePreferredObjectStoreId(selectedToolPreferredObjectStoreId) {
size="sm"
class="float-right"
@click="onShowObjectStoreSelect">
-
+
{
let axiosMock;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
- axiosMock.onGet("/api/object_store?selectable=true").reply(200, OBJECT_STORES);
});
afterEach(async () => {
@@ -50,6 +47,7 @@ describe("UserPreferredObjectStore.vue", () => {
const wrapper = mountComponent();
const el = await wrapper.find(ROOT_COMPONENT.preferences.object_store.selector);
await el.trigger("click");
+ await flushPromises();
const els = wrapper.findAll(ROOT_COMPONENT.preferences.object_store_selection.option_buttons.selector);
expect(els.length).toBe(3);
const galaxyDefaultOption = wrapper.find(
@@ -63,7 +61,7 @@ describe("UserPreferredObjectStore.vue", () => {
expect(errorEl.exists()).toBeFalsy();
});
- it("updates object store to default on selection null", async () => {
+ it("updates object store to default on actual selection", async () => {
const wrapper = mountComponent();
const el = await wrapper.find(ROOT_COMPONENT.preferences.object_store.selector);
await el.trigger("click");
diff --git a/client/src/components/Workflow/Run/WorkflowStorageConfiguration.vue b/client/src/components/Workflow/Run/WorkflowStorageConfiguration.vue
index b5bc04252618..3d5a62022754 100644
--- a/client/src/components/Workflow/Run/WorkflowStorageConfiguration.vue
+++ b/client/src/components/Workflow/Run/WorkflowStorageConfiguration.vue
@@ -5,7 +5,7 @@
class="workflow-storage-indicator workflow-storage-indicator-primary"
v-bind="buttonProps"
@click="showPreferredObjectStoreModal = true">
-
+
-
+
[];
+ "application/json": components["schemas"]["ConcreteObjectStoreModel"][];
};
};
/** @description Validation Error */
@@ -12441,7 +12512,7 @@ export interface operations {
};
};
show_info_api_object_store__object_store_id__get: {
- /** Return boolean to indicate if Galaxy's default object store allows selection. */
+ /** Get information about a concrete object store configured with Galaxy. */
parameters: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
@@ -12453,10 +12524,10 @@ export interface operations {
};
};
responses: {
- /** @description A list with details about the remote files available to the user. */
+ /** @description Successful Response */
200: {
content: {
- "application/json": Record;
+ "application/json": components["schemas"]["ConcreteObjectStoreModel"];
};
};
/** @description Validation Error */
@@ -13950,6 +14021,62 @@ export interface operations {
};
};
};
+ get_user_usage_api_users__user_id__usage_get: {
+ /** Return the user's quota usage summary broken down by quota source */
+ parameters: {
+ /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
+ header?: {
+ "run-as"?: string;
+ };
+ /** @description The ID of the user to get or __current__. */
+ path: {
+ user_id: string;
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["UserQuotaUsage"][];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ get_user_usage_for_label_api_users__user_id__usage__label__get: {
+ /** Return the user's quota usage summary for a given quota source label */
+ parameters: {
+ /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
+ header?: {
+ "run-as"?: string;
+ };
+ /** @description The ID of the user to get or __current__. */
+ /** @description The label corresponding to the quota source to fetch usage information about. */
+ path: {
+ user_id: string;
+ label: string;
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ content: {
+ "application/json": components["schemas"]["UserQuotaUsage"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
version_api_version_get: {
/**
* Return Galaxy version information: major/minor version, optional extra info
diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py
index 7e3c6c3c589d..2c58dd7e1052 100644
--- a/lib/galaxy/managers/users.py
+++ b/lib/galaxy/managers/users.py
@@ -34,6 +34,7 @@
base,
deletable,
)
+from galaxy.model import UserQuotaUsage
from galaxy.security.validate_user_input import (
VALID_EMAIL_RE,
validate_email,
@@ -650,22 +651,38 @@ def add_serializers(self):
}
)
- def serialize_disk_usage(self, user: model.User) -> List[Dict[str, Any]]:
- rval = user.dictify_usage(self.app.object_store)
- for usage in rval:
- quota_source_label = usage["quota_source_label"]
- usage["quota_percent"] = self.user_manager.quota(user, quota_source_label=quota_source_label)
- usage["quota"] = self.user_manager.quota(user, total=True, quota_source_label=quota_source_label)
- usage["quota_bytes"] = self.user_manager.quota_bytes(user, quota_source_label=quota_source_label)
+ def serialize_disk_usage(self, user: model.User) -> List[UserQuotaUsage]:
+ usages = user.dictify_usage(self.app.object_store)
+ rval: List[UserQuotaUsage] = []
+ for usage in usages:
+ quota_source_label = usage.quota_source_label
+ quota_percent = self.user_manager.quota(user, quota_source_label=quota_source_label)
+ quota = self.user_manager.quota(user, total=True, quota_source_label=quota_source_label)
+ quota_bytes = self.user_manager.quota_bytes(user, quota_source_label=quota_source_label)
+ rval.append(
+ UserQuotaUsage(
+ quota_source_label=quota_source_label,
+ total_disk_usage=usage.total_disk_usage,
+ quota_percent=quota_percent,
+ quota=quota,
+ quota_bytes=quota_bytes,
+ )
+ )
return rval
- def serialize_disk_usage_for(self, user: model.User, label: Optional[str]) -> Dict[str, Any]:
+ def serialize_disk_usage_for(self, user: model.User, label: Optional[str]) -> UserQuotaUsage:
usage = user.dictify_usage_for(label)
- quota_source_label = usage["quota_source_label"]
- usage["quota_percent"] = self.user_manager.quota(user, quota_source_label=quota_source_label)
- usage["quota"] = self.user_manager.quota(user, total=True, quota_source_label=quota_source_label)
- usage["quota_bytes"] = self.user_manager.quota_bytes(user, quota_source_label=quota_source_label)
- return usage
+ quota_source_label = usage.quota_source_label
+ quota_percent = self.user_manager.quota(user, quota_source_label=quota_source_label)
+ quota = self.user_manager.quota(user, total=True, quota_source_label=quota_source_label)
+ quota_bytes = self.user_manager.quota_bytes(user, quota_source_label=quota_source_label)
+ return UserQuotaUsage(
+ quota_source_label=quota_source_label,
+ total_disk_usage=usage.total_disk_usage,
+ quota_percent=quota_percent,
+ quota=quota,
+ quota_bytes=quota_bytes,
+ )
class UserDeserializer(base.ModelDeserializer):
diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py
index 723c2e0fadf0..32d7e01275cd 100644
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -40,6 +40,7 @@
import sqlalchemy
from boltons.iterutils import remap
+from pydantic import BaseModel
from social_core.storage import (
AssociationMixin,
CodeMixin,
@@ -634,6 +635,19 @@ def calculate_user_disk_usage_statements(user_id, quota_source_map, for_sqlite=F
return statements
+# move these to galaxy.schema.schema once galaxy-data depends on
+# galaxy-schema.
+class UserQuotaBasicUsage(BaseModel):
+ quota_source_label: Optional[str]
+ total_disk_usage: float
+
+
+class UserQuotaUsage(UserQuotaBasicUsage):
+ quota_percent: Optional[float]
+ quota_bytes: Optional[int]
+ quota: Optional[str]
+
+
class User(Base, Dictifiable, RepresentById):
"""
Data for a Galaxy user or admin and relations to their
@@ -1025,23 +1039,23 @@ def attempt_create_private_role(self):
session.add(assoc)
session.flush()
- def dictify_usage(self, object_store=None) -> List[Dict[str, Any]]:
+ def dictify_usage(self, object_store=None) -> List[UserQuotaBasicUsage]:
"""Include object_store to include empty/unused usage info."""
used_labels: Set[Union[str, None]] = set()
- rval: List[Dict[str, Any]] = [
- {
- "quota_source_label": None,
- "total_disk_usage": float(self.disk_usage or 0),
- }
+ rval: List[UserQuotaBasicUsage] = [
+ UserQuotaBasicUsage(
+ quota_source_label=None,
+ total_disk_usage=float(self.disk_usage or 0),
+ )
]
used_labels.add(None)
for quota_source_usage in self.quota_source_usages:
label = quota_source_usage.quota_source_label
rval.append(
- {
- "quota_source_label": label,
- "total_disk_usage": float(quota_source_usage.disk_usage),
- }
+ UserQuotaBasicUsage(
+ quota_source_label=label,
+ total_disk_usage=float(quota_source_usage.disk_usage),
+ )
)
used_labels.add(label)
@@ -1049,33 +1063,33 @@ def dictify_usage(self, object_store=None) -> List[Dict[str, Any]]:
for label in object_store.get_quota_source_map().ids_per_quota_source().keys():
if label not in used_labels:
rval.append(
- {
- "quota_source_label": label,
- "total_disk_usage": 0.0,
- }
+ UserQuotaBasicUsage(
+ quota_source_label=label,
+ total_disk_usage=0.0,
+ )
)
return rval
- def dictify_usage_for(self, quota_source_label: Optional[str]) -> Dict[str, Any]:
- rval: Dict[str, Any]
+ def dictify_usage_for(self, quota_source_label: Optional[str]) -> UserQuotaBasicUsage:
+ rval: UserQuotaBasicUsage
if quota_source_label is None:
- rval = {
- "quota_source_label": None,
- "total_disk_usage": float(self.disk_usage or 0),
- }
+ rval = UserQuotaBasicUsage(
+ quota_source_label=None,
+ total_disk_usage=float(self.disk_usage or 0),
+ )
else:
quota_source_usage = self.quota_source_usage_for(quota_source_label)
if quota_source_usage is None:
- rval = {
- "quota_source_label": quota_source_label,
- "total_disk_usage": 0.0,
- }
+ rval = UserQuotaBasicUsage(
+ quota_source_label=quota_source_label,
+ total_disk_usage=0.0,
+ )
else:
- rval = {
- "quota_source_label": quota_source_label,
- "total_disk_usage": float(quota_source_usage.disk_usage),
- }
+ rval = UserQuotaBasicUsage(
+ quota_source_label=quota_source_label,
+ total_disk_usage=float(quota_source_usage.disk_usage),
+ )
return rval
diff --git a/lib/galaxy/objectstore/__init__.py b/lib/galaxy/objectstore/__init__.py
index 45793c187f92..d0b0e35b4c05 100644
--- a/lib/galaxy/objectstore/__init__.py
+++ b/lib/galaxy/objectstore/__init__.py
@@ -24,6 +24,11 @@
)
import yaml
+from pydantic import BaseModel
+from typing_extensions import (
+ Literal,
+ TypedDict,
+)
from galaxy.exceptions import (
ObjectInvalid,
@@ -52,6 +57,22 @@
log = logging.getLogger(__name__)
+BadgeSourceT = Literal["admin", "galaxy"]
+BadgeT = Literal[
+ "faster",
+ "slower",
+ "short_term",
+ "cloud",
+ "backed_up",
+ "not_backed_up",
+ "more_secure",
+ "less_secure",
+ "more_stable",
+ "less_stable",
+ "quota",
+ "no_quota",
+ "restricted",
+]
BADGE_SPECIFICATION = [
{"type": "faster", "conflicts": ["slower"]},
@@ -69,6 +90,12 @@
BADGE_SPECIFCATION_BY_TYPE = {s["type"]: s for s in BADGE_SPECIFICATION}
+class BadgeDict(TypedDict):
+ type: BadgeT
+ message: Optional[str]
+ source: BadgeSourceT
+
+
class ObjectStore(metaclass=abc.ABCMeta):
"""ObjectStore interface.
@@ -270,7 +297,7 @@ def get_concrete_store_description_markdown(self, obj):
"""
@abc.abstractmethod
- def get_concrete_store_badges(self, obj):
+ def get_concrete_store_badges(self, obj) -> List[BadgeDict]:
"""Return a list of dictified badges summarizing the object store configuration."""
@abc.abstractmethod
@@ -443,7 +470,7 @@ def get_concrete_store_name(self, obj):
def get_concrete_store_description_markdown(self, obj):
return self._invoke("get_concrete_store_description_markdown", obj)
- def get_concrete_store_badges(self, obj) -> List[Dict[str, Any]]:
+ def get_concrete_store_badges(self, obj) -> List[BadgeDict]:
return self._invoke("get_concrete_store_badges", obj)
def get_store_usage_percent(self):
@@ -549,14 +576,27 @@ def to_dict(self):
rval["badges"] = self._get_concrete_store_badges(None)
return rval
- def _get_concrete_store_badges(self, obj) -> List[Dict[str, Any]]:
- badge_dicts: List[Dict[str, Any]] = []
+ def to_model(self, object_store_id: str) -> "ConcreteObjectStoreModel":
+ return ConcreteObjectStoreModel(
+ object_store_id=object_store_id,
+ private=self.private,
+ name=self.name,
+ description=self.description,
+ quota=QuotaModel(source=self.quota_source, enabled=self.quota_enabled),
+ badges=self._get_concrete_store_badges(None),
+ )
+
+ def _get_concrete_store_badges(self, obj) -> List[BadgeDict]:
+ badge_dicts: List[BadgeDict] = []
for badge in self.badges:
- badge_dict = badge.copy()
- badge_dict["source"] = "admin"
+ badge_dict: BadgeDict = {
+ "source": "admin",
+ "type": badge["type"],
+ "message": badge["message"],
+ }
badge_dicts.append(badge_dict)
- quota_badge_dict: Dict[str, Any]
+ quota_badge_dict: BadgeDict
if self.galaxy_enable_quotas and self.quota_enabled:
quota_badge_dict = {
"type": "quota",
@@ -571,7 +611,7 @@ def _get_concrete_store_badges(self, obj) -> List[Dict[str, Any]]:
}
badge_dicts.append(quota_badge_dict)
if self.private:
- restricted_badge_dict = {
+ restricted_badge_dict: BadgeDict = {
"type": "restricted",
"message": None,
"source": "galaxy",
@@ -981,7 +1021,7 @@ def _get_concrete_store_name(self, obj):
def _get_concrete_store_description_markdown(self, obj):
return self._call_method("_get_concrete_store_description_markdown", obj, None, False)
- def _get_concrete_store_badges(self, obj):
+ def _get_concrete_store_badges(self, obj) -> List[BadgeDict]:
return self._call_method("_get_concrete_store_badges", obj, [], False)
def _is_private(self, obj):
@@ -1348,6 +1388,20 @@ def get_quota_source_map(self):
return quota_source_map
+class QuotaModel(BaseModel):
+ source: Optional[str]
+ enabled: bool
+
+
+class ConcreteObjectStoreModel(BaseModel):
+ object_store_id: Optional[str]
+ private: bool
+ name: Optional[str]
+ description: Optional[str]
+ quota: QuotaModel
+ badges: List[BadgeDict]
+
+
def type_to_object_store_class(store: str, fsmon: bool = False) -> Tuple[Type[BaseObjectStore], Dict[str, Any]]:
objectstore_class: Type[BaseObjectStore]
objectstore_constructor_kwds = {}
diff --git a/lib/galaxy/quota/__init__.py b/lib/galaxy/quota/__init__.py
index b01cf47d5ae6..5102f263cd95 100644
--- a/lib/galaxy/quota/__init__.py
+++ b/lib/galaxy/quota/__init__.py
@@ -1,5 +1,6 @@
"""Galaxy Quotas"""
import logging
+from typing import Optional
from sqlalchemy.sql import text
@@ -23,10 +24,10 @@ class QuotaAgent: # metaclass=abc.ABCMeta
"""
# TODO: make abstractmethod after they work better with mypy
- def get_quota(self, user, quota_source_label=None):
+ def get_quota(self, user, quota_source_label=None) -> Optional[int]:
"""Return quota in bytes or None if no quota is set."""
- def get_quota_nice_size(self, user, quota_source_label=None):
+ def get_quota_nice_size(self, user, quota_source_label=None) -> Optional[str]:
"""Return quota as a human-readable string or 'unlimited' if no quota is set."""
quota_bytes = self.get_quota(user, quota_source_label=quota_source_label)
if quota_bytes is not None:
@@ -36,10 +37,12 @@ def get_quota_nice_size(self, user, quota_source_label=None):
return quota_str
# TODO: make abstractmethod after they work better with mypy
- def get_percent(self, trans=None, user=False, history=False, usage=False, quota=False, quota_source_label=None):
+ def get_percent(
+ self, trans=None, user=False, history=False, usage=False, quota=False, quota_source_label=None
+ ) -> Optional[int]:
"""Return the percentage of any storage quota applicable to the user/transaction."""
- def get_usage(self, trans=None, user=False, history=False, quota_source_label=None):
+ def get_usage(self, trans=None, user=False, history=False, quota_source_label=None) -> Optional[float]:
if trans:
user = trans.user
history = trans.history
@@ -73,14 +76,16 @@ class NoQuotaAgent(QuotaAgent):
def __init__(self):
pass
- def get_quota(self, user, quota_source_label=None):
+ def get_quota(self, user, quota_source_label=None) -> Optional[int]:
return None
@property
def default_quota(self):
return None
- def get_percent(self, trans=None, user=False, history=False, usage=False, quota=False, quota_source_label=None):
+ def get_percent(
+ self, trans=None, user=False, history=False, usage=False, quota=False, quota_source_label=None
+ ) -> Optional[int]:
return None
def is_over_quota(self, app, job, job_destination):
@@ -94,7 +99,7 @@ def __init__(self, model):
self.model = model
self.sa_session = model.context
- def get_quota(self, user, quota_source_label=None):
+ def get_quota(self, user, quota_source_label=None) -> Optional[int]:
"""
Calculated like so:
@@ -220,7 +225,9 @@ def set_default_quota(self, default_type, quota):
self.sa_session.add(target_default)
self.sa_session.flush()
- def get_percent(self, trans=None, user=False, history=False, usage=False, quota=False, quota_source_label=None):
+ def get_percent(
+ self, trans=None, user=False, history=False, usage=False, quota=False, quota_source_label=None
+ ) -> Optional[int]:
"""
Return the percentage of any storage quota applicable to the user/transaction.
"""
diff --git a/lib/galaxy/webapps/galaxy/api/object_store.py b/lib/galaxy/webapps/galaxy/api/object_store.py
index f53e267339a2..3e3a81442d04 100644
--- a/lib/galaxy/webapps/galaxy/api/object_store.py
+++ b/lib/galaxy/webapps/galaxy/api/object_store.py
@@ -2,11 +2,7 @@
API operations on Galaxy's object store.
"""
import logging
-from typing import (
- Any,
- Dict,
- List,
-)
+from typing import List
from fastapi import (
Path,
@@ -18,7 +14,10 @@
RequestParameterInvalidException,
)
from galaxy.managers.context import ProvidesUserContext
-from galaxy.objectstore import BaseObjectStore
+from galaxy.objectstore import (
+ BaseObjectStore,
+ ConcreteObjectStoreModel,
+)
from . import (
depends,
DependsOnTrans,
@@ -34,7 +33,9 @@
)
SelectableQueryParam: bool = Query(
- default=False, title="Selectable", description="Restrict index query to user selectable object stores."
+ default=False,
+ title="Selectable",
+ description="Restrict index query to user selectable object stores, the current implementation requires this to be true.",
)
@@ -44,37 +45,34 @@ class FastAPIObjectStore:
@router.get(
"/api/object_store",
- summary="",
- response_description="",
+ summary="Get a list of (currently only concrete) object stores configured with this Galaxy instance.",
+ response_description="A list of the configured object stores.",
)
def index(
self,
trans: ProvidesUserContext = DependsOnTrans,
selectable: bool = SelectableQueryParam,
- ) -> List[Dict[str, Any]]:
+ ) -> List[ConcreteObjectStoreModel]:
if not selectable:
raise RequestParameterInvalidException(
"The object store index query currently needs to be called with selectable=true"
)
selectable_ids = self.object_store.object_store_ids_allowing_selection()
- return [self._dict_for(selectable_id) for selectable_id in selectable_ids]
+ return [self._model_for(selectable_id) for selectable_id in selectable_ids]
@router.get(
"/api/object_store/{object_store_id}",
- summary="Return boolean to indicate if Galaxy's default object store allows selection.",
- response_description="A list with details about the remote files available to the user.",
+ summary="Get information about a concrete object store configured with Galaxy.",
)
def show_info(
self,
trans: ProvidesUserContext = DependsOnTrans,
object_store_id: str = ConcreteObjectStoreIdPathParam,
- ) -> Dict[str, Any]:
- return self._dict_for(object_store_id)
+ ) -> ConcreteObjectStoreModel:
+ return self._model_for(object_store_id)
- def _dict_for(self, object_store_id: str) -> Dict[str, Any]:
+ def _model_for(self, object_store_id: str) -> ConcreteObjectStoreModel:
concrete_object_store = self.object_store.get_concrete_store_by_object_store_id(object_store_id)
if concrete_object_store is None:
raise ObjectNotFound()
- as_dict = concrete_object_store.to_dict()
- as_dict["object_store_id"] = object_store_id
- return as_dict
+ return concrete_object_store.to_model(object_store_id)
diff --git a/lib/galaxy/webapps/galaxy/api/users.py b/lib/galaxy/webapps/galaxy/api/users.py
index 016faee817c9..82a1a75e5561 100644
--- a/lib/galaxy/webapps/galaxy/api/users.py
+++ b/lib/galaxy/webapps/galaxy/api/users.py
@@ -5,7 +5,10 @@
import json
import logging
import re
-from typing import Optional
+from typing import (
+ List,
+ Optional,
+)
from fastapi import (
Body,
@@ -34,6 +37,7 @@
from galaxy.model import (
User,
UserAddress,
+ UserQuotaUsage,
)
from galaxy.schema import APIKeyModel
from galaxy.schema.fields import DecodedDatabaseIdField
@@ -74,11 +78,18 @@
UserIdPathParam: DecodedDatabaseIdField = Path(..., title="User ID", description="The ID of the user to get.")
APIKeyPathParam: str = Path(..., title="API Key", description="The API key of the user.")
+FlexibleUserIdPathParam: str = Path(..., title="User ID", description="The ID of the user to get or __current__.")
+QuotaSourceLabelPathParam: str = Path(
+ ...,
+ title="Quota Source Label",
+ description="The label corresponding to the quota source to fetch usage information about.",
+)
@router.cbv
class FastAPIUsers:
service: UsersService = depends(UsersService)
+ user_serializer: users.UserSerializer = depends(users.UserSerializer)
@router.put(
"/api/users/recalculate_disk_usage",
@@ -141,6 +152,44 @@ def delete_api_key(
self.service.delete_api_key(trans, user_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
+ @router.get(
+ "/api/users/{user_id}/usage",
+ name="get_user_usage",
+ summary="Return the user's quota usage summary broken down by quota source",
+ )
+ def usage(
+ self,
+ trans: ProvidesUserContext = DependsOnTrans,
+ user_id: str = FlexibleUserIdPathParam,
+ ) -> List[UserQuotaUsage]:
+ user = get_user_full(trans, user_id, False)
+ if user:
+ rval = self.user_serializer.serialize_disk_usage(user)
+ return rval
+ else:
+ return []
+
+ @router.get(
+ "/api/users/{user_id}/usage/{label}",
+ name="get_user_usage_for_label",
+ summary="Return the user's quota usage summary for a given quota source label",
+ )
+ def usage_for(
+ self,
+ trans: ProvidesUserContext = DependsOnTrans,
+ user_id: str = FlexibleUserIdPathParam,
+ label: str = QuotaSourceLabelPathParam,
+ ) -> Optional[UserQuotaUsage]:
+ user = get_user_full(trans, user_id, False)
+ effective_label: Optional[str] = label
+ if label == "__null__":
+ effective_label = None
+ if user:
+ rval = self.user_serializer.serialize_disk_usage_for(user, effective_label)
+ return rval
+ else:
+ return None
+
@router.get(
"/api/users/{user_id}/beacon",
summary="Returns information about beacon share settings",
@@ -287,65 +336,7 @@ def _get_user_full(self, trans, user_id, **kwd):
"""Return referenced user or None if anonymous user is referenced."""
deleted = kwd.get("deleted", "False")
deleted = util.string_as_bool(deleted)
- try:
- # user is requesting data about themselves
- if user_id == "current":
- # ...and is anonymous - return usage and quota (if any)
- if not trans.user:
- return None
-
- # ...and is logged in - return full
- else:
- user = trans.user
- else:
- return managers_base.get_object(
- trans,
- user_id,
- "User",
- deleted=deleted,
- )
- # check that the user is requesting themselves (and they aren't del'd) unless admin
- if not trans.user_is_admin:
- if trans.user != user or user.deleted:
- raise exceptions.InsufficientPermissionsException(
- "You are not allowed to perform action on that user", id=user_id
- )
- return user
- except exceptions.MessageException:
- raise
- except Exception:
- raise exceptions.RequestParameterInvalidException("Invalid user id specified", id=user_id)
-
- @expose_api
- def usage(self, trans, user_id: str, **kwd):
- """
- GET /api/users/{user_id}/usage
-
- Get user's disk usage broken down by quota source.
- """
- user = self._get_user_full(trans, user_id, **kwd)
- if user:
- rval = self.user_serializer.serialize_disk_usage(user)
- return rval
- else:
- return []
-
- @expose_api
- def usage_for(self, trans, user_id: str, label: str, **kwd):
- """
- GET /api/users/{user_id}/usage/{label}
-
- Get user's disk usage for supplied quota source label.
- """
- user = self._get_user_full(trans, user_id, **kwd)
- effective_label: Optional[str] = label
- if label == "__null__":
- effective_label = None
- if user:
- rval = self.user_serializer.serialize_disk_usage_for(user, effective_label)
- return rval
- else:
- return None
+ return get_user_full(trans, user_id, deleted)
@expose_api
def create(self, trans: GalaxyWebTransaction, payload: dict, **kwd):
@@ -1153,3 +1144,34 @@ def _get_user(self, trans, id):
if user != trans.user and not trans.user_is_admin:
raise exceptions.InsufficientPermissionsException("Access denied.")
return user
+
+
+def get_user_full(trans: ProvidesUserContext, user_id: str, deleted: bool) -> Optional[User]:
+ try:
+ # user is requesting data about themselves
+ if user_id == "current":
+ # ...and is anonymous - return usage and quota (if any)
+ if not trans.user:
+ return None
+
+ # ...and is logged in - return full
+ else:
+ user = trans.user
+ else:
+ return managers_base.get_object(
+ trans,
+ user_id,
+ "User",
+ deleted=deleted,
+ )
+ # check that the user is requesting themselves (and they aren't del'd) unless admin
+ if not trans.user_is_admin:
+ if trans.user != user or user.deleted:
+ raise exceptions.InsufficientPermissionsException(
+ "You are not allowed to perform action on that user", id=user_id
+ )
+ return user
+ except exceptions.MessageException:
+ raise
+ except Exception:
+ raise exceptions.RequestParameterInvalidException("Invalid user id specified", id=user_id)
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index 79c2e9098ba9..a9bcaba11314 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -583,12 +583,6 @@ def populate_api_routes(webapp, app):
conditions=dict(method=["POST"]),
)
- webapp.mapper.connect(
- "/api/users/{user_id}/usage", action="usage", controller="users", conditions=dict(method=["GET"])
- )
- webapp.mapper.connect(
- "/api/users/{user_id}/usage/{label}", action="usage_for", controller="users", conditions=dict(method=["GET"])
- )
webapp.mapper.resource_with_deleted("user", "users", path_prefix="/api")
webapp.mapper.resource("visualization", "visualizations", path_prefix="/api")
webapp.mapper.resource("plugins", "plugins", path_prefix="/api")
diff --git a/test/unit/data/test_quota.py b/test/unit/data/test_quota.py
index ebdc256c5b65..508476c47c12 100644
--- a/test/unit/data/test_quota.py
+++ b/test/unit/data/test_quota.py
@@ -60,8 +60,8 @@ def test_calculate_usage_per_source(self):
usages = self.u.dictify_usage()
assert len(usages) == 2
- assert usages[1]["quota_source_label"] == "myquotalabel"
- assert usages[1]["total_disk_usage"] == 114
+ assert usages[1].quota_source_label == "myquotalabel"
+ assert usages[1].total_disk_usage == 114
class TestCalculateUsage(BaseModelTestCase):
@@ -144,23 +144,23 @@ def test_calculate_usage_alt_quota(self):
model.context.refresh(u)
usages = u.dictify_usage(object_store)
assert len(usages) == 2
- assert usages[0]["quota_source_label"] is None
- assert usages[0]["total_disk_usage"] == 10
+ assert usages[0].quota_source_label is None
+ assert usages[0].total_disk_usage == 10
- assert usages[1]["quota_source_label"] == "alt_source"
- assert usages[1]["total_disk_usage"] == 15
+ assert usages[1].quota_source_label == "alt_source"
+ assert usages[1].total_disk_usage == 15
usage = u.dictify_usage_for(None)
- assert usage["quota_source_label"] is None
- assert usage["total_disk_usage"] == 10
+ assert usage.quota_source_label is None
+ assert usage.total_disk_usage == 10
usage = u.dictify_usage_for("alt_source")
- assert usage["quota_source_label"] == "alt_source"
- assert usage["total_disk_usage"] == 15
+ assert usage.quota_source_label == "alt_source"
+ assert usage.total_disk_usage == 15
usage = u.dictify_usage_for("unused_source")
- assert usage["quota_source_label"] == "unused_source"
- assert usage["total_disk_usage"] == 0
+ assert usage.quota_source_label == "unused_source"
+ assert usage.total_disk_usage == 0
def test_calculate_usage_removes_unused_quota_labels(self):
model = self.model
@@ -180,22 +180,22 @@ def test_calculate_usage_removes_unused_quota_labels(self):
model.context.refresh(u)
usages = u.dictify_usage()
assert len(usages) == 2
- assert usages[0]["quota_source_label"] is None
- assert usages[0]["total_disk_usage"] == 10
+ assert usages[0].quota_source_label is None
+ assert usages[0].total_disk_usage == 10
- assert usages[1]["quota_source_label"] == "alt_source"
- assert usages[1]["total_disk_usage"] == 15
+ assert usages[1].quota_source_label == "alt_source"
+ assert usages[1].total_disk_usage == 15
alt_source.default_quota_source = "new_alt_source"
u.calculate_and_set_disk_usage(object_store)
model.context.refresh(u)
usages = u.dictify_usage()
assert len(usages) == 2
- assert usages[0]["quota_source_label"] is None
- assert usages[0]["total_disk_usage"] == 10
+ assert usages[0].quota_source_label is None
+ assert usages[0].total_disk_usage == 10
- assert usages[1]["quota_source_label"] == "new_alt_source"
- assert usages[1]["total_disk_usage"] == 15
+ assert usages[1].quota_source_label == "new_alt_source"
+ assert usages[1].total_disk_usage == 15
def test_dictify_usage_unused_quota_labels(self):
model = self.model
@@ -236,11 +236,11 @@ def test_calculate_usage_default_storage_disabled(self):
model.context.refresh(u)
usages = u.dictify_usage(object_store)
assert len(usages) == 2
- assert usages[0]["quota_source_label"] is None
- assert usages[0]["total_disk_usage"] == 0
+ assert usages[0].quota_source_label is None
+ assert usages[0].total_disk_usage == 0
- assert usages[1]["quota_source_label"] == "alt_source"
- assert usages[1]["total_disk_usage"] == 15
+ assert usages[1].quota_source_label == "alt_source"
+ assert usages[1].total_disk_usage == 15
class TestQuota(BaseModelTestCase):
@@ -377,5 +377,5 @@ def test_labeled_usage(self):
usages = u.dictify_usage()
assert len(usages) == 2
- assert usages[1]["quota_source_label"] == "foobar"
- assert usages[1]["total_disk_usage"] == 247
+ assert usages[1].quota_source_label == "foobar"
+ assert usages[1].total_disk_usage == 247