|
29 | 29 | DynamicServiceStart, |
30 | 30 | DynamicServiceStop, |
31 | 31 | ) |
32 | | -from models_library.api_schemas_webserver.projects import ProjectGet, ProjectPatch |
| 32 | +from models_library.api_schemas_webserver.projects import ( |
| 33 | + ProjectDocument, |
| 34 | + ProjectGet, |
| 35 | + ProjectPatch, |
| 36 | +) |
33 | 37 | from models_library.basic_types import KeyIDStr |
34 | 38 | from models_library.errors import ErrorDict |
35 | 39 | from models_library.groups import GroupID |
36 | 40 | from models_library.products import ProductName |
37 | | -from models_library.projects import ( |
38 | | - Project, |
39 | | - ProjectID, |
40 | | -) |
| 41 | +from models_library.projects import Project, ProjectID, ProjectTemplateType |
| 42 | +from models_library.projects import ProjectType as ProjectTypeAPI |
41 | 43 | from models_library.projects_access import Owner |
42 | 44 | from models_library.projects_nodes import Node, NodeState, PartialNode |
43 | 45 | from models_library.projects_nodes_io import NodeID, NodeIDStr, PortLink |
|
82 | 84 | ServiceWasNotFoundError, |
83 | 85 | ) |
84 | 86 | from servicelib.redis import ( |
| 87 | + PROJECT_DB_UPDATE_REDIS_LOCK_KEY, |
85 | 88 | exclusive, |
86 | 89 | get_project_locked_state, |
| 90 | + increment_and_return_project_document_version, |
87 | 91 | is_project_locked, |
88 | 92 | with_project_locked, |
89 | 93 | ) |
|
103 | 107 | from ..products import products_web |
104 | 108 | from ..rabbitmq import get_rabbitmq_rpc_client |
105 | 109 | from ..redis import ( |
| 110 | + get_redis_document_manager_client_sdk, |
106 | 111 | get_redis_lock_manager_client_sdk, |
107 | 112 | ) |
108 | 113 | from ..resource_manager.user_sessions import ( |
|
142 | 147 | from ._nodes_utils import set_reservation_same_as_limit, validate_new_service_resources |
143 | 148 | from ._projects_repository_legacy import APP_PROJECT_DBAPI, ProjectDBAPI |
144 | 149 | from ._projects_repository_legacy_utils import PermissionStr |
| 150 | +from ._socketio import notify_project_document_updated |
145 | 151 | from .exceptions import ( |
146 | 152 | ClustersKeeperNotAvailableError, |
147 | 153 | DefaultPricingUnitNotFoundError, |
|
169 | 175 | PROJECT_REDIS_LOCK_KEY: str = "project:{}" |
170 | 176 |
|
171 | 177 |
|
| 178 | +async def patch_project_and_notify_users( |
| 179 | + app: web.Application, |
| 180 | + *, |
| 181 | + project_uuid: ProjectID, |
| 182 | + patch_project_data: dict[str, Any], |
| 183 | + user_primary_gid: GroupID, |
| 184 | +) -> None: |
| 185 | + """ |
| 186 | + Patches a project and notifies users involved in the project with version control. |
| 187 | +
|
| 188 | + This function performs the following operations atomically: |
| 189 | + 1. Patches the project in the database |
| 190 | + 2. Retrieves the updated project with workbench |
| 191 | + 3. Creates a project document |
| 192 | + 4. Increments the document version |
| 193 | + 5. Notifies users about the project update |
| 194 | +
|
| 195 | + Args: |
| 196 | + app: The web application instance |
| 197 | + project_uuid: The project UUID to patch |
| 198 | + patch_project_data: Dictionary containing the project data to patch |
| 199 | + user_primary_gid: Primary group ID of the user making the change |
| 200 | +
|
| 201 | + Note: |
| 202 | + This function is decorated with Redis exclusive lock to ensure |
| 203 | + thread-safe operations on the project document. |
| 204 | + """ |
| 205 | + |
| 206 | + @exclusive( |
| 207 | + get_redis_lock_manager_client_sdk(app), |
| 208 | + lock_key=PROJECT_DB_UPDATE_REDIS_LOCK_KEY.format(project_uuid), |
| 209 | + blocking=True, |
| 210 | + blocking_timeout=datetime.timedelta(seconds=30), |
| 211 | + ) |
| 212 | + async def _patch_and_notify() -> None: |
| 213 | + await _projects_repository.patch_project( |
| 214 | + app=app, |
| 215 | + project_uuid=project_uuid, |
| 216 | + new_partial_project_data=patch_project_data, |
| 217 | + ) |
| 218 | + project_with_workbench = await _projects_repository.get_project_with_workbench( |
| 219 | + app=app, project_uuid=project_uuid |
| 220 | + ) |
| 221 | + project_document = ProjectDocument( |
| 222 | + uuid=project_with_workbench.uuid, |
| 223 | + workspace_id=project_with_workbench.workspace_id, |
| 224 | + name=project_with_workbench.name, |
| 225 | + description=project_with_workbench.description, |
| 226 | + thumbnail=project_with_workbench.thumbnail, |
| 227 | + last_change_date=project_with_workbench.last_change_date, |
| 228 | + classifiers=project_with_workbench.classifiers, |
| 229 | + dev=project_with_workbench.dev, |
| 230 | + quality=project_with_workbench.quality, |
| 231 | + workbench=project_with_workbench.workbench, |
| 232 | + ui=project_with_workbench.ui, |
| 233 | + type=cast(ProjectTypeAPI, project_with_workbench.type), |
| 234 | + template_type=cast( |
| 235 | + ProjectTemplateType, project_with_workbench.template_type |
| 236 | + ), |
| 237 | + ) |
| 238 | + redis_client_sdk = get_redis_document_manager_client_sdk(app) |
| 239 | + document_version = await increment_and_return_project_document_version( |
| 240 | + redis_client=redis_client_sdk, project_uuid=project_uuid |
| 241 | + ) |
| 242 | + await notify_project_document_updated( |
| 243 | + app=app, |
| 244 | + project_id=project_uuid, |
| 245 | + user_primary_gid=user_primary_gid, |
| 246 | + version=document_version, |
| 247 | + document=project_document, |
| 248 | + ) |
| 249 | + |
| 250 | + await _patch_and_notify() |
| 251 | + |
| 252 | + |
172 | 253 | def _is_node_dynamic(node_key: str) -> bool: |
173 | 254 | return "/dynamic/" in node_key |
174 | 255 |
|
@@ -349,10 +430,10 @@ async def patch_project_for_user( |
349 | 430 |
|
350 | 431 | # 5. If patching template type |
351 | 432 | if new_template_type := patch_project_data.get("template_type"): |
352 | | - # 4.1 Check if user is a tester |
| 433 | + # 5.1 Check if user is a tester |
353 | 434 | if UserRole(current_user["role"]) < UserRole.TESTER: |
354 | 435 | raise InsufficientRoleForProjectTemplateTypeUpdateError |
355 | | - # 4.2 Check the compatibility of the template type with the project |
| 436 | + # 5.2 Check the compatibility of the template type with the project |
356 | 437 | if project_db.type == ProjectType.STANDARD and new_template_type is not None: |
357 | 438 | raise ProjectTypeAndTemplateIncompatibilityError( |
358 | 439 | project_uuid=project_uuid, |
|
0 commit comments