Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class ProjectPatch(InputSchema):
),
] = None
quality: dict[str, Any] | None = None
template_type: ProjectTemplateType | None = None

def to_domain_model(self) -> dict[str, Any]:
return self.model_dump(exclude_unset=True, by_alias=False)
Expand Down
8 changes: 4 additions & 4 deletions packages/models-library/src/models_library/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from datetime import datetime
from enum import Enum, auto
from enum import Enum
from typing import Annotated, Any, Final, TypeAlias
from uuid import UUID

Expand Down Expand Up @@ -54,9 +54,9 @@ class ProjectType(str, Enum):


class ProjectTemplateType(StrAutoEnum):
TEMPLATE = auto()
TUTORIAL = auto()
HYPERTOOL = auto()
TEMPLATE = "TEMPLATE"
TUTORIAL = "TUTORIAL"
HYPERTOOL = "HYPERTOOL"


class BaseProjectModel(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3962,6 +3962,14 @@ paths:
schema:
$ref: '#/components/schemas/ProjectTypeAPI'
default: all
- name: template_type
in: query
required: false
schema:
anyOf:
- $ref: '#/components/schemas/ProjectTemplateType'
- type: 'null'
title: Template Type
- name: show_hidden
in: query
required: false
Expand Down Expand Up @@ -14868,6 +14876,10 @@ components:
- type: object
- type: 'null'
title: Quality
templateType:
anyOf:
- $ref: '#/components/schemas/ProjectTemplateType'
- type: 'null'
type: object
title: ProjectPatch
ProjectPermalink:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from ..exceptions import (
ClustersKeeperNotAvailableError,
DefaultPricingUnitNotFoundError,
InsufficientRoleForProjectTemplateTypeUpdateError,
NodeNotFoundError,
ParentNodeNotFoundError,
ProjectDeleteError,
Expand All @@ -40,6 +41,7 @@
ProjectOwnerNotFoundInTheProjectAccessRightsError,
ProjectStartsTooManyDynamicNodesError,
ProjectTooManyProjectOpenedError,
ProjectTypeAndTemplateIncompatibilityError,
ProjectWalletPendingTransactionError,
WrongTagIdsInQueryError,
)
Expand Down Expand Up @@ -88,6 +90,10 @@
status.HTTP_403_FORBIDDEN,
"Do not have sufficient access rights on project {project_uuid} for this action",
),
InsufficientRoleForProjectTemplateTypeUpdateError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Do not have sufficient access rights on updating project template type",
),
ProjectInvalidUsageError: HttpErrorInfo(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"Invalid usage for project",
Expand Down Expand Up @@ -124,6 +130,10 @@
status.HTTP_400_BAD_REQUEST,
"Wrong tag IDs in query",
),
ProjectTypeAndTemplateIncompatibilityError: HttpErrorInfo(
status.HTTP_400_BAD_REQUEST,
"Wrong project type and template type combination: {reason}",
),
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ async def list_projects(request: web.Request):
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
project_type=query_params.project_type,
template_type=query_params.template_type,
show_hidden=query_params.show_hidden,
trashed=query_params.filters.trashed,
folder_id=query_params.folder_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from models_library.basic_types import IDStr
from models_library.folders import FolderID
from models_library.projects import ProjectID
from models_library.projects import ProjectID, ProjectTemplateType
from models_library.projects_nodes_io import NodeID
from models_library.rest_base import RequestParameters
from models_library.rest_filters import Filters, FiltersQueryParameters
Expand Down Expand Up @@ -136,6 +136,7 @@ class ProjectFilters(Filters):

class ProjectsListExtraQueryParams(RequestParameters):
project_type: Annotated[ProjectTypeAPI, Field(alias="type")] = ProjectTypeAPI.all
template_type: Annotated[ProjectTemplateType | None, Field(...)] = None
show_hidden: Annotated[
bool, Field(description="includes projects marked as hidden in the listing")
] = False
Expand Down Expand Up @@ -167,6 +168,22 @@ def _search_check_empty_string(cls, v):
return None
return v

_template_type_null_or_none_str_to_none_validator = field_validator(
"template_type", mode="before"
)(null_or_none_str_to_none_validator)

@model_validator(mode="after")
def check_template_type_compatibility(self):
if (
self.project_type in [ProjectTypeAPI.all, ProjectTypeAPI.user]
and self.template_type is not None
):
msg = (
"When project type is `all` or `user` the template_type should be None"
)
raise ValueError(msg)
return self

_null_or_none_str_to_none_validator = field_validator("folder_id", mode="before")(
null_or_none_str_to_none_validator
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from aiohttp import web
from models_library.folders import FolderID, FolderQuery, FolderScope
from models_library.projects import ProjectID
from models_library.projects import ProjectID, ProjectTemplateType
from models_library.rest_ordering import OrderBy
from models_library.users import UserID
from models_library.workspaces import WorkspaceID, WorkspaceQuery, WorkspaceScope
Expand Down Expand Up @@ -91,6 +91,7 @@ async def list_projects( # pylint: disable=too-many-arguments
folder_id: FolderID | None,
# attrs filter
project_type: ProjectTypeAPI,
template_type: ProjectTemplateType | None,
show_hidden: bool, # NOTE: Be careful, this filters only hidden projects
trashed: bool | None,
# search
Expand Down Expand Up @@ -148,6 +149,7 @@ async def list_projects( # pylint: disable=too-many-arguments
),
# attrs
filter_by_project_type=ProjectTypeAPI.to_project_type_db(project_type),
filter_by_template_type=template_type,
filter_by_services=user_available_services,
filter_trashed=trashed,
filter_hidden=show_hidden,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ def _create_shared_workspace_query(
def _create_attributes_filters(
*,
filter_by_project_type: ProjectType | None,
filter_by_template_type: ProjectTemplateType | None,
filter_hidden: bool | None,
filter_published: bool | None,
filter_trashed: bool | None,
Expand All @@ -595,6 +596,11 @@ def _create_attributes_filters(
if filter_by_project_type is not None:
attributes_filters.append(projects.c.type == filter_by_project_type.value)

if filter_by_template_type is not None:
attributes_filters.append(
projects.c.template_type == filter_by_template_type.value
)

if filter_hidden is not None:
attributes_filters.append(projects.c.hidden.is_(filter_hidden))

Expand Down Expand Up @@ -647,6 +653,7 @@ async def list_projects_dicts( # pylint: disable=too-many-arguments,too-many-st
folder_query: FolderQuery,
# attribute filters
filter_by_project_type: ProjectType | None = None,
filter_by_template_type: ProjectTemplateType | None = None,
filter_by_services: list[dict] | None = None,
filter_published: bool | None = None,
filter_hidden: bool | None = False,
Expand Down Expand Up @@ -694,6 +701,7 @@ async def list_projects_dicts( # pylint: disable=too-many-arguments,too-many-st

attributes_filters = self._create_attributes_filters(
filter_by_project_type=filter_by_project_type,
filter_by_template_type=filter_by_template_type,
filter_hidden=filter_hidden,
filter_published=filter_published,
filter_trashed=filter_trashed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
from .exceptions import (
ClustersKeeperNotAvailableError,
DefaultPricingUnitNotFoundError,
InsufficientRoleForProjectTemplateTypeUpdateError,
InvalidEC2TypeInResourcesSpecsError,
InvalidKeysInResourcesSpecsError,
NodeNotFoundError,
Expand All @@ -150,6 +151,7 @@
ProjectOwnerNotFoundInTheProjectAccessRightsError,
ProjectStartsTooManyDynamicNodesError,
ProjectTooManyProjectOpenedError,
ProjectTypeAndTemplateIncompatibilityError,
)
from .models import ProjectDict, ProjectPatchInternalExtended
from .settings import ProjectsSettings, get_plugin_settings
Expand Down Expand Up @@ -327,7 +329,27 @@ async def patch_project(
if new_prj_access_rights[_prj_owner_primary_group] != _prj_required_permissions:
raise ProjectOwnerNotFoundInTheProjectAccessRightsError

# 4. Patch the project
# 4. If patching template type
if new_template_type := patch_project_data.get("template_type"):
# 4.1 Check if user is a tester
current_user: dict = await get_user(app, user_id)
if UserRole(current_user["role"]) < UserRole.TESTER:
raise InsufficientRoleForProjectTemplateTypeUpdateError
# 4.2 Check the compatibility of the template type with the project
if project_db.type == ProjectType.STANDARD and new_template_type is not None:
raise ProjectTypeAndTemplateIncompatibilityError(
project_uuid=project_uuid,
project_type=project_db.type,
project_template=new_template_type,
)
if project_db.type == ProjectType.TEMPLATE and new_template_type is None:
raise ProjectTypeAndTemplateIncompatibilityError(
project_uuid=project_uuid,
project_type=project_db.type,
project_template=new_template_type,
)

# 5. Patch the project
await _projects_repository.patch_project(
app=app,
project_uuid=project_uuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ def debug_message(self):
return f"{self.code}: {self}"


class ProjectInvalidUsageError(BaseProjectError):
...
class ProjectInvalidUsageError(BaseProjectError): ...


class ProjectOwnerNotFoundInTheProjectAccessRightsError(BaseProjectError):
Expand Down Expand Up @@ -83,8 +82,17 @@ class ProjectsBatchDeleteError(BaseProjectError):
msg_template = "One or more projects could not be deleted in the batch: {errors}"


class ProjectTrashError(BaseProjectError):
...
class ProjectsPatchError(BaseProjectError): ...


class ProjectTypeAndTemplateIncompatibilityError(ProjectsPatchError):
msg_template = "Patching project '{project_uuid}' type {project_type} and template {project_template} is not allowed"


class InsufficientRoleForProjectTemplateTypeUpdateError(ProjectsPatchError): ...


class ProjectTrashError(BaseProjectError): ...


class ProjectStoppingError(ProjectTrashError):
Expand Down Expand Up @@ -146,12 +154,10 @@ def __init__(self, *, max_num_projects: int, **ctx):
self.max_num_projects = max_num_projects


class PermalinkNotAllowedError(BaseProjectError):
...
class PermalinkNotAllowedError(BaseProjectError): ...


class PermalinkFactoryError(BaseProjectError):
...
class PermalinkFactoryError(BaseProjectError): ...


class ProjectNodeResourcesInvalidError(BaseProjectError):
Expand All @@ -178,12 +184,10 @@ class InvalidEC2TypeInResourcesSpecsError(ProjectNodeResourcesInvalidError):
)


class ProjectNodeResourcesInsufficientRightsError(BaseProjectError):
...
class ProjectNodeResourcesInsufficientRightsError(BaseProjectError): ...


class ProjectNodeRequiredInputsNotSetError(BaseProjectError):
...
class ProjectNodeRequiredInputsNotSetError(BaseProjectError): ...


class ProjectNodeConnectionsMissingError(ProjectNodeRequiredInputsNotSetError):
Expand Down
Loading
Loading