11import asyncio
2+ import datetime
23import logging
34
45import arrow
56from aiohttp import web
7+ from models_library .basic_types import IDStr
68from models_library .products import ProductName
79from models_library .projects import ProjectID
10+ from models_library .rest_ordering import OrderBy , OrderDirection
811from models_library .users import UserID
912from servicelib .aiohttp .application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
1013from servicelib .common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
1114from servicelib .utils import fire_and_forget_task
1215
1316from ..director_v2 import api as director_v2_api
1417from ..dynamic_scheduler import api as dynamic_scheduler_api
15- from . import projects_service
18+ from . import _crud_api_read , projects_service
1619from ._access_rights_api import check_user_project_permission
17- from .exceptions import ProjectRunningConflictError
20+ from .exceptions import (
21+ ProjectNotFoundError ,
22+ ProjectNotTrashedError ,
23+ ProjectRunningConflictError ,
24+ )
1825from .models import ProjectPatchInternalExtended
1926
2027_logger = logging .getLogger (__name__ )
@@ -45,7 +52,7 @@ async def trash_project(
4552 project_id : ProjectID ,
4653 force_stop_first : bool ,
4754 explicit : bool ,
48- ):
55+ ) -> None :
4956 """
5057
5158 Raises:
@@ -108,7 +115,7 @@ async def untrash_project(
108115 product_name : ProductName ,
109116 user_id : UserID ,
110117 project_id : ProjectID ,
111- ):
118+ ) -> None :
112119 # NOTE: check_user_project_permission is inside projects_api.patch_project
113120 await projects_service .patch_project (
114121 app ,
@@ -119,3 +126,90 @@ async def untrash_project(
119126 trashed_at = None , trashed_explicitly = False , trashed_by = None
120127 ),
121128 )
129+
130+
131+ async def list_trashed_projects (
132+ app : web .Application ,
133+ * ,
134+ product_name : ProductName ,
135+ user_id : UserID ,
136+ until_equal_datetime : datetime .datetime | None = None ,
137+ ) -> list [ProjectID ]:
138+ """
139+ Lists all projects that were trashed until a specific datetime.
140+ """
141+ projects , _ = await _crud_api_read .list_projects_full_depth (
142+ request = app ,
143+ user_id = user_id ,
144+ product_name = product_name ,
145+ trashed = True ,
146+ tag_ids_list = [],
147+ offset = 0 ,
148+ limit = 100 , # FIXME: this is only the first 100. redo with yield
149+ order_by = OrderBy (field = IDStr ("trashed" ), direction = OrderDirection .ASC ),
150+ search_by_multi_columns = None ,
151+ search_by_project_name = None ,
152+ )
153+
154+ # NOTE; this can be done at the database level when projects_repo is refactored
155+ # by defining a custom trash_filter that permits some flexibility in the filtering options
156+ trashed_projects = []
157+ for project in projects :
158+ trashed_at = project .get ("trashed_at" )
159+ trashed_by = project .get ("trashed_by" )
160+ trashed_explicitly = project .get ("trashed_explicitly" )
161+
162+ if (
163+ trashed_at
164+ and (until_equal_datetime is None or trashed_at < until_equal_datetime )
165+ and trashed_by == user_id
166+ and trashed_explicitly
167+ ):
168+ trashed_projects .append (project )
169+
170+ return [project ["uuid" ] for project in trashed_projects ]
171+
172+
173+ async def delete_trashed_project (
174+ app : web .Application ,
175+ * ,
176+ user_id : UserID ,
177+ project_id : ProjectID ,
178+ until_equal_datetime : datetime .datetime | None = None ,
179+ ) -> None :
180+ """
181+ Deletes a project that was explicitly trashed by the user from a specific datetime (if provided, otherwise all).
182+
183+ Raises:
184+ ProjectNotFoundError: If the project is not found.
185+ ProjectNotTrashedError: If the project was not trashed explicitly by the user from the specified datetime.
186+ """
187+ project = await projects_service .get_project_for_user (
188+ app , project_uuid = f"{ project_id } " , user_id = user_id
189+ )
190+
191+ if not project :
192+ raise ProjectNotFoundError (project_uuid = project_id , user_id = user_id )
193+
194+ trashed_at = project .get ("trashed_at" )
195+ trashed_by = project .get ("trashed_by" )
196+ trashed_explicitly = project .get ("trashed_explicitly" )
197+
198+ if (
199+ not trashed_at
200+ or (until_equal_datetime is not None and until_equal_datetime < trashed_at )
201+ or trashed_by != user_id
202+ or not trashed_explicitly
203+ ):
204+ raise ProjectNotTrashedError (
205+ project_uuid = project_id ,
206+ user_id = user_id ,
207+ reason = "Project was not trashed explicitly by the user from the specified datetime" ,
208+ )
209+ # FIXME: locking while deleting?
210+ # FIXME: this can be heavy. Rather call delete_trashed_project as a task
211+ await projects_service .delete_project_by_user (
212+ app ,
213+ user_id = user_id ,
214+ project_uuid = project_id ,
215+ )
0 commit comments