mirror of
https://github.com/clearml/clearml-server
synced 2025-06-26 23:15:47 +00:00
Optimize and improve tasks/models/projects.delete
This commit is contained in:
parent
8b464e7ae6
commit
f5008d80ad
@ -38,7 +38,6 @@ class ModelRequest(models.Base):
|
|||||||
|
|
||||||
class DeleteModelRequest(ModelRequest):
|
class DeleteModelRequest(ModelRequest):
|
||||||
force = fields.BoolField(default=False)
|
force = fields.BoolField(default=False)
|
||||||
return_file_url = fields.BoolField(default=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PublishModelRequest(ModelRequest):
|
class PublishModelRequest(ModelRequest):
|
||||||
|
@ -6,7 +6,12 @@ from apiserver.database.model import EntityVisibility
|
|||||||
|
|
||||||
|
|
||||||
class ProjectReq(models.Base):
|
class ProjectReq(models.Base):
|
||||||
project = fields.StringField()
|
project = fields.StringField(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRequest(ProjectReq):
|
||||||
|
force = fields.BoolField(default=False)
|
||||||
|
delete_contents = fields.BoolField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class GetHyperParamReq(ProjectReq):
|
class GetHyperParamReq(ProjectReq):
|
||||||
|
@ -73,6 +73,7 @@ class EnqueueRequest(UpdateRequest):
|
|||||||
class DeleteRequest(UpdateRequest):
|
class DeleteRequest(UpdateRequest):
|
||||||
move_to_trash = BoolField(default=True)
|
move_to_trash = BoolField(default=True)
|
||||||
return_file_urls = BoolField(default=False)
|
return_file_urls = BoolField(default=False)
|
||||||
|
delete_output_models = BoolField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class SetRequirementsRequest(TaskRequest):
|
class SetRequirementsRequest(TaskRequest):
|
||||||
@ -140,6 +141,7 @@ class DeleteArtifactsRequest(TaskRequest):
|
|||||||
class ResetRequest(UpdateRequest):
|
class ResetRequest(UpdateRequest):
|
||||||
clear_all = BoolField(default=False)
|
clear_all = BoolField(default=False)
|
||||||
return_file_urls = BoolField(default=False)
|
return_file_urls = BoolField(default=False)
|
||||||
|
delete_output_models = BoolField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class MultiTaskRequest(models.Base):
|
class MultiTaskRequest(models.Base):
|
||||||
|
@ -943,3 +943,20 @@ class EventBLL(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return es_res.get("deleted", 0)
|
return es_res.get("deleted", 0)
|
||||||
|
|
||||||
|
def delete_multi_task_events(self, company_id: str, task_ids: Sequence[str]):
|
||||||
|
"""
|
||||||
|
Delete mutliple task events. No check is done for tasks write access
|
||||||
|
so it should be checked by the calling code
|
||||||
|
"""
|
||||||
|
es_req = {"query": {"terms": {"task": task_ids}}}
|
||||||
|
with translate_errors_context(), TimingContext("es", "delete_multi_tasks_events"):
|
||||||
|
es_res = delete_company_events(
|
||||||
|
es=self.es,
|
||||||
|
company_id=company_id,
|
||||||
|
event_type=EventType.all,
|
||||||
|
body=es_req,
|
||||||
|
refresh=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return es_res.get("deleted", 0)
|
||||||
|
137
apiserver/bll/project/project_cleanup.py
Normal file
137
apiserver/bll/project/project_cleanup.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Tuple, Set
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
from apiserver.apierrors import errors
|
||||||
|
from apiserver.bll.event import EventBLL
|
||||||
|
from apiserver.bll.task.task_cleanup import (
|
||||||
|
collect_debug_image_urls,
|
||||||
|
collect_plot_image_urls,
|
||||||
|
TaskUrls,
|
||||||
|
)
|
||||||
|
from apiserver.config_repo import config
|
||||||
|
from apiserver.database.model import EntityVisibility
|
||||||
|
from apiserver.database.model.model import Model
|
||||||
|
from apiserver.database.model.project import Project
|
||||||
|
from apiserver.database.model.task.task import Task, ArtifactModes
|
||||||
|
from apiserver.timing_context import TimingContext
|
||||||
|
|
||||||
|
log = config.logger(__file__)
|
||||||
|
event_bll = EventBLL()
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class DeleteProjectResult:
|
||||||
|
deleted: int = 0
|
||||||
|
disassociated_tasks: int = 0
|
||||||
|
deleted_models: int = 0
|
||||||
|
deleted_tasks: int = 0
|
||||||
|
urls: TaskUrls = None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_project(
|
||||||
|
company: str, project_id: str, force: bool, delete_contents: bool
|
||||||
|
) -> DeleteProjectResult:
|
||||||
|
project = Project.get_for_writing(company=company, id=project_id)
|
||||||
|
if not project:
|
||||||
|
raise errors.bad_request.InvalidProjectId(id=project_id)
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
for cls, error in (
|
||||||
|
(Task, errors.bad_request.ProjectHasTasks),
|
||||||
|
(Model, errors.bad_request.ProjectHasModels),
|
||||||
|
):
|
||||||
|
non_archived = cls.objects(
|
||||||
|
project=project_id, system_tags__nin=[EntityVisibility.archived.value],
|
||||||
|
).only("id")
|
||||||
|
if non_archived:
|
||||||
|
raise error("use force=true to delete", id=project_id)
|
||||||
|
|
||||||
|
if not delete_contents:
|
||||||
|
with TimingContext("mongo", "update_children"):
|
||||||
|
for cls in (Model, Task):
|
||||||
|
updated_count = cls.objects(project=project_id).update(project=None)
|
||||||
|
res = DeleteProjectResult(disassociated_tasks=updated_count)
|
||||||
|
else:
|
||||||
|
deleted_models, model_urls = _delete_models(project=project_id)
|
||||||
|
deleted_tasks, event_urls, artifact_urls = _delete_tasks(
|
||||||
|
company=company, project=project_id
|
||||||
|
)
|
||||||
|
res = DeleteProjectResult(
|
||||||
|
deleted_tasks=deleted_tasks,
|
||||||
|
deleted_models=deleted_models,
|
||||||
|
urls=TaskUrls(
|
||||||
|
model_urls=list(model_urls),
|
||||||
|
event_urls=list(event_urls),
|
||||||
|
artifact_urls=list(artifact_urls),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
res.deleted = Project.objects(id=project_id).delete()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_tasks(company: str, project: str) -> Tuple[int, Set, Set]:
|
||||||
|
"""
|
||||||
|
Delete only the task themselves and their non published version.
|
||||||
|
Child models under the same project are deleted separately.
|
||||||
|
Children tasks should be deleted in the same api call.
|
||||||
|
If any child entities are left in another projects then updated their parent task to None
|
||||||
|
"""
|
||||||
|
tasks = Task.objects(project=project).only("id", "execution__artifacts")
|
||||||
|
if not tasks:
|
||||||
|
return 0, set(), set()
|
||||||
|
|
||||||
|
task_ids = {t.id for t in tasks}
|
||||||
|
with TimingContext("mongo", "delete_tasks_update_children"):
|
||||||
|
Task.objects(parent__in=task_ids, project__ne=project).update(parent=None)
|
||||||
|
Model.objects(task__in=task_ids, project__ne=project).update(task=None)
|
||||||
|
|
||||||
|
event_urls, artifact_urls = set(), set()
|
||||||
|
for task in tasks:
|
||||||
|
event_urls.update(collect_debug_image_urls(company, task.id))
|
||||||
|
event_urls.update(collect_plot_image_urls(company, task.id))
|
||||||
|
if task.execution and task.execution.artifacts:
|
||||||
|
artifact_urls.update(
|
||||||
|
{
|
||||||
|
a.uri
|
||||||
|
for a in task.execution.artifacts.values()
|
||||||
|
if a.mode == ArtifactModes.output and a.uri
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
event_bll.delete_multi_task_events(company, list(task_ids))
|
||||||
|
deleted = tasks.delete()
|
||||||
|
return deleted, event_urls, artifact_urls
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_models(project: str) -> Tuple[int, Set[str]]:
|
||||||
|
"""
|
||||||
|
Delete project models and update the tasks from other projects
|
||||||
|
that reference them to reference None.
|
||||||
|
"""
|
||||||
|
with TimingContext("mongo", "delete_models"):
|
||||||
|
models = Model.objects(project=project).only("task", "id", "uri")
|
||||||
|
if not models:
|
||||||
|
return 0, set()
|
||||||
|
|
||||||
|
model_ids = {m.id for m in models}
|
||||||
|
Task.objects(execution__model__in=model_ids, project__ne=project).update(
|
||||||
|
execution__model=None
|
||||||
|
)
|
||||||
|
|
||||||
|
model_tasks = {m.task for m in models if m.task}
|
||||||
|
if model_tasks:
|
||||||
|
now = datetime.utcnow()
|
||||||
|
Task.objects(
|
||||||
|
id__in=model_tasks, project__ne=project, output__model__in=model_ids
|
||||||
|
).update(
|
||||||
|
output__model=None,
|
||||||
|
output__error=f"model deleted on {now.isoformat()}",
|
||||||
|
last_change=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
urls = {m.uri for m in models if m.uri}
|
||||||
|
deleted = models.delete()
|
||||||
|
return deleted, urls
|
@ -38,7 +38,7 @@ from apiserver.timing_context import TimingContext
|
|||||||
from apiserver.utilities.parameter_key_escaper import ParameterKeyEscaper
|
from apiserver.utilities.parameter_key_escaper import ParameterKeyEscaper
|
||||||
from .artifacts import artifacts_prepare_for_save
|
from .artifacts import artifacts_prepare_for_save
|
||||||
from .param_utils import params_prepare_for_save
|
from .param_utils import params_prepare_for_save
|
||||||
from .utils import ChangeStatusRequest, validate_status_change, update_project_time, task_deleted_prefix
|
from .utils import ChangeStatusRequest, validate_status_change, update_project_time, deleted_prefix
|
||||||
|
|
||||||
log = config.logger(__file__)
|
log = config.logger(__file__)
|
||||||
org_bll = OrgBLL()
|
org_bll = OrgBLL()
|
||||||
@ -249,7 +249,7 @@ class TaskBLL:
|
|||||||
with TimingContext("mongo", "clone task"):
|
with TimingContext("mongo", "clone task"):
|
||||||
parent_task = (
|
parent_task = (
|
||||||
task.parent
|
task.parent
|
||||||
if task.parent and not task.parent.startswith(task_deleted_prefix)
|
if task.parent and not task.parent.startswith(deleted_prefix)
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
new_task = Task(
|
new_task = Task(
|
||||||
|
@ -11,7 +11,7 @@ from apiserver.apierrors import errors
|
|||||||
from apiserver.bll.event import EventBLL
|
from apiserver.bll.event import EventBLL
|
||||||
from apiserver.bll.event.event_bll import PlotFields
|
from apiserver.bll.event.event_bll import PlotFields
|
||||||
from apiserver.bll.event.event_common import EventType
|
from apiserver.bll.event.event_common import EventType
|
||||||
from apiserver.bll.task.utils import task_deleted_prefix
|
from apiserver.bll.task.utils import deleted_prefix
|
||||||
from apiserver.database.model.model import Model
|
from apiserver.database.model.model import Model
|
||||||
from apiserver.database.model.task.task import Task, TaskStatus, ArtifactModes
|
from apiserver.database.model.task.task import Task, TaskStatus, ArtifactModes
|
||||||
from apiserver.timing_context import TimingContext
|
from apiserver.timing_context import TimingContext
|
||||||
@ -81,7 +81,7 @@ class CleanupResult:
|
|||||||
urls: TaskUrls = None
|
urls: TaskUrls = None
|
||||||
|
|
||||||
|
|
||||||
def _collect_plot_image_urls(company: str, task: str) -> Set[str]:
|
def collect_plot_image_urls(company: str, task: str) -> Set[str]:
|
||||||
urls = set()
|
urls = set()
|
||||||
next_scroll_id = None
|
next_scroll_id = None
|
||||||
with TimingContext("es", "collect_plot_image_urls"):
|
with TimingContext("es", "collect_plot_image_urls"):
|
||||||
@ -99,7 +99,7 @@ def _collect_plot_image_urls(company: str, task: str) -> Set[str]:
|
|||||||
return urls
|
return urls
|
||||||
|
|
||||||
|
|
||||||
def _collect_debug_image_urls(company: str, task: str) -> Set[str]:
|
def collect_debug_image_urls(company: str, task: str) -> Set[str]:
|
||||||
"""
|
"""
|
||||||
Return the set of unique image urls
|
Return the set of unique image urls
|
||||||
Uses DebugImagesIterator to make sure that we do not retrieve recycled urls
|
Uses DebugImagesIterator to make sure that we do not retrieve recycled urls
|
||||||
@ -132,7 +132,11 @@ def _collect_debug_image_urls(company: str, task: str) -> Set[str]:
|
|||||||
|
|
||||||
|
|
||||||
def cleanup_task(
|
def cleanup_task(
|
||||||
task: Task, force: bool = False, update_children=True, return_file_urls=False
|
task: Task,
|
||||||
|
force: bool = False,
|
||||||
|
update_children=True,
|
||||||
|
return_file_urls=False,
|
||||||
|
delete_output_models=True,
|
||||||
) -> CleanupResult:
|
) -> CleanupResult:
|
||||||
"""
|
"""
|
||||||
Validate task deletion and delete/modify all its output.
|
Validate task deletion and delete/modify all its output.
|
||||||
@ -144,8 +148,8 @@ def cleanup_task(
|
|||||||
|
|
||||||
event_urls, artifact_urls, model_urls = set(), set(), set()
|
event_urls, artifact_urls, model_urls = set(), set(), set()
|
||||||
if return_file_urls:
|
if return_file_urls:
|
||||||
event_urls = _collect_debug_image_urls(task.company, task.id)
|
event_urls = collect_debug_image_urls(task.company, task.id)
|
||||||
event_urls.update(_collect_plot_image_urls(task.company, task.id))
|
event_urls.update(collect_plot_image_urls(task.company, task.id))
|
||||||
if task.execution and task.execution.artifacts:
|
if task.execution and task.execution.artifacts:
|
||||||
artifact_urls = {
|
artifact_urls = {
|
||||||
a.uri
|
a.uri
|
||||||
@ -154,7 +158,7 @@ def cleanup_task(
|
|||||||
}
|
}
|
||||||
model_urls = {m.uri for m in models.draft.objects().only("uri") if m.uri}
|
model_urls = {m.uri for m in models.draft.objects().only("uri") if m.uri}
|
||||||
|
|
||||||
deleted_task_id = f"{task_deleted_prefix}{task.id}"
|
deleted_task_id = f"{deleted_prefix}{task.id}"
|
||||||
if update_children:
|
if update_children:
|
||||||
with TimingContext("mongo", "update_task_children"):
|
with TimingContext("mongo", "update_task_children"):
|
||||||
updated_children = Task.objects(parent=task.id).update(
|
updated_children = Task.objects(parent=task.id).update(
|
||||||
@ -163,7 +167,7 @@ def cleanup_task(
|
|||||||
else:
|
else:
|
||||||
updated_children = 0
|
updated_children = 0
|
||||||
|
|
||||||
if models.draft:
|
if models.draft and delete_output_models:
|
||||||
with TimingContext("mongo", "delete_models"):
|
with TimingContext("mongo", "delete_models"):
|
||||||
deleted_models = models.draft.objects().delete()
|
deleted_models = models.draft.objects().delete()
|
||||||
else:
|
else:
|
||||||
|
@ -13,7 +13,7 @@ from apiserver.timing_context import TimingContext
|
|||||||
from apiserver.utilities.attrs import typed_attrs
|
from apiserver.utilities.attrs import typed_attrs
|
||||||
|
|
||||||
valid_statuses = get_options(TaskStatus)
|
valid_statuses = get_options(TaskStatus)
|
||||||
task_deleted_prefix = "__DELETED__"
|
deleted_prefix = "__DELETED__"
|
||||||
|
|
||||||
|
|
||||||
@typed_attrs
|
@typed_attrs
|
||||||
|
@ -155,6 +155,7 @@ class Task(AttributedDocument):
|
|||||||
"active_duration",
|
"active_duration",
|
||||||
"parent",
|
"parent",
|
||||||
"project",
|
"project",
|
||||||
|
"execution.model",
|
||||||
("company", "name"),
|
("company", "name"),
|
||||||
("company", "user"),
|
("company", "user"),
|
||||||
("company", "status", "type"),
|
("company", "status", "type"),
|
||||||
|
@ -698,14 +698,6 @@ delete {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"2.13": ${delete."2.1"} {
|
"2.13": ${delete."2.1"} {
|
||||||
request {
|
|
||||||
properties {
|
|
||||||
return_file_url {
|
|
||||||
description: "If set to 'true' then return the url of the model file. Default value is 'false'"
|
|
||||||
type: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response {
|
response {
|
||||||
properties {
|
properties {
|
||||||
url {
|
url {
|
||||||
|
@ -242,6 +242,23 @@ _definitions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
urls {
|
||||||
|
type: object
|
||||||
|
properties {
|
||||||
|
model_urls {
|
||||||
|
type: array
|
||||||
|
items {type: string}
|
||||||
|
}
|
||||||
|
event_urls {
|
||||||
|
type: array
|
||||||
|
items {type: string}
|
||||||
|
}
|
||||||
|
artifact_urls {
|
||||||
|
type: array
|
||||||
|
items {type: string}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
create {
|
create {
|
||||||
@ -515,6 +532,32 @@ delete {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"2.13": ${delete."2.1"} {
|
||||||
|
request {
|
||||||
|
properties {
|
||||||
|
delete_contents {
|
||||||
|
description: "If set to 'true' then the project tasks and models will be deleted. Otherwise their project property will be unassigned. Default value is 'false'"
|
||||||
|
type: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response {
|
||||||
|
properties {
|
||||||
|
urls {
|
||||||
|
description: "The urls of the files that were uploaded by the project tasks and models. Returned if the 'delete_contents' was set to 'true'"
|
||||||
|
"$ref": "#/definitions/urls"
|
||||||
|
}
|
||||||
|
deleted_models {
|
||||||
|
description: "Number of models deleted"
|
||||||
|
type: integer
|
||||||
|
}
|
||||||
|
deleted_tasks {
|
||||||
|
description: "Number of tasks deleted"
|
||||||
|
type: integer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
get_unique_metric_variants {
|
get_unique_metric_variants {
|
||||||
"2.1" {
|
"2.1" {
|
||||||
|
@ -533,6 +533,23 @@ _definitions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
task_urls {
|
||||||
|
type: object
|
||||||
|
properties {
|
||||||
|
model_urls {
|
||||||
|
type: array
|
||||||
|
items {type: string}
|
||||||
|
}
|
||||||
|
event_urls {
|
||||||
|
type: array
|
||||||
|
items {type: string}
|
||||||
|
}
|
||||||
|
artifact_urls {
|
||||||
|
type: array
|
||||||
|
items {type: string}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_by_id {
|
get_by_id {
|
||||||
@ -1203,9 +1220,8 @@ reset {
|
|||||||
response {
|
response {
|
||||||
properties {
|
properties {
|
||||||
urls {
|
urls {
|
||||||
description: "The urls of the files that were uploaded by this task. Returned if the 'return_file_urls' properties was set to True"
|
description: "The urls of the files that were uploaded by this task. Returned if the 'return_file_urls' was set to 'true'"
|
||||||
type: array
|
"$ref": "#/definitions/task_urls"
|
||||||
items {type: string}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1277,9 +1293,8 @@ delete {
|
|||||||
response {
|
response {
|
||||||
properties {
|
properties {
|
||||||
urls {
|
urls {
|
||||||
description: "The urls of the files that were uploaded by this task. Returned if the 'return_file_urls' properties was set to True"
|
description: "The urls of the files that were uploaded by this task. Returned if the 'return_file_urls' was set to 'true'"
|
||||||
type: array
|
"$ref": "#/definitions/task_urls"
|
||||||
items {type: string}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ from apiserver.bll.model import ModelBLL
|
|||||||
from apiserver.bll.organization import OrgBLL, Tags
|
from apiserver.bll.organization import OrgBLL, Tags
|
||||||
from apiserver.bll.project import ProjectBLL
|
from apiserver.bll.project import ProjectBLL
|
||||||
from apiserver.bll.task import TaskBLL
|
from apiserver.bll.task import TaskBLL
|
||||||
|
from apiserver.bll.task.utils import deleted_prefix
|
||||||
from apiserver.config_repo import config
|
from apiserver.config_repo import config
|
||||||
from apiserver.database.errors import translate_errors_context
|
from apiserver.database.errors import translate_errors_context
|
||||||
from apiserver.database.model import validate_id
|
from apiserver.database.model import validate_id
|
||||||
@ -442,7 +443,7 @@ def set_ready(call: APICall, company_id, req_model: PublishModelRequest):
|
|||||||
|
|
||||||
|
|
||||||
@endpoint("models.delete", request_data_model=DeleteModelRequest)
|
@endpoint("models.delete", request_data_model=DeleteModelRequest)
|
||||||
def update(call: APICall, company_id, request: DeleteModelRequest):
|
def delete(call: APICall, company_id, request: DeleteModelRequest):
|
||||||
model_id = request.model
|
model_id = request.model
|
||||||
force = request.force
|
force = request.force
|
||||||
|
|
||||||
@ -452,7 +453,7 @@ def update(call: APICall, company_id, request: DeleteModelRequest):
|
|||||||
if not model:
|
if not model:
|
||||||
raise errors.bad_request.InvalidModelId(**query)
|
raise errors.bad_request.InvalidModelId(**query)
|
||||||
|
|
||||||
deleted_model_id = f"__DELETED__{model_id}"
|
deleted_model_id = f"{deleted_prefix}{model_id}"
|
||||||
|
|
||||||
using_tasks = Task.objects(execution__model=model_id).only("id")
|
using_tasks = Task.objects(execution__model=model_id).only("id")
|
||||||
if using_tasks:
|
if using_tasks:
|
||||||
@ -473,21 +474,19 @@ def update(call: APICall, company_id, request: DeleteModelRequest):
|
|||||||
raise errors.bad_request.ModelCreatingTaskExists(
|
raise errors.bad_request.ModelCreatingTaskExists(
|
||||||
"and published, use force=True to delete", task=model.task
|
"and published, use force=True to delete", task=model.task
|
||||||
)
|
)
|
||||||
now = datetime.utcnow()
|
if task.output and task.output.model == model_id:
|
||||||
task.update(
|
now = datetime.utcnow()
|
||||||
output__model=deleted_model_id,
|
task.update(
|
||||||
output__error=f"model deleted on {now.isoformat()}",
|
output__model=deleted_model_id,
|
||||||
last_change=now,
|
output__error=f"model deleted on {now.isoformat()}",
|
||||||
upsert=False,
|
last_change=now,
|
||||||
)
|
upsert=False,
|
||||||
|
)
|
||||||
|
|
||||||
del_count = Model.objects(**query).delete()
|
del_count = Model.objects(**query).delete()
|
||||||
if del_count:
|
if del_count:
|
||||||
_reset_cached_tags(company_id, projects=[model.project])
|
_reset_cached_tags(company_id, projects=[model.project])
|
||||||
call.result.data = dict(
|
call.result.data = dict(deleted=del_count > 0, url=model.uri,)
|
||||||
deleted=del_count > 0,
|
|
||||||
url=model.uri if request.return_file_url else None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@endpoint("models.make_public", min_version="2.9", request_data_model=MakePublicRequest)
|
@endpoint("models.make_public", min_version="2.9", request_data_model=MakePublicRequest)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
import attr
|
||||||
from mongoengine import Q
|
from mongoengine import Q
|
||||||
|
|
||||||
from apiserver.apierrors import errors
|
from apiserver.apierrors import errors
|
||||||
@ -12,15 +14,14 @@ from apiserver.apimodels.projects import (
|
|||||||
ProjectTaskParentsRequest,
|
ProjectTaskParentsRequest,
|
||||||
ProjectHyperparamValuesRequest,
|
ProjectHyperparamValuesRequest,
|
||||||
ProjectsGetRequest,
|
ProjectsGetRequest,
|
||||||
|
DeleteRequest,
|
||||||
)
|
)
|
||||||
from apiserver.bll.organization import OrgBLL, Tags
|
from apiserver.bll.organization import OrgBLL, Tags
|
||||||
from apiserver.bll.project import ProjectBLL
|
from apiserver.bll.project import ProjectBLL
|
||||||
|
from apiserver.bll.project.project_cleanup import delete_project
|
||||||
from apiserver.bll.task import TaskBLL
|
from apiserver.bll.task import TaskBLL
|
||||||
from apiserver.database.errors import translate_errors_context
|
from apiserver.database.errors import translate_errors_context
|
||||||
from apiserver.database.model import EntityVisibility
|
|
||||||
from apiserver.database.model.model import Model
|
|
||||||
from apiserver.database.model.project import Project
|
from apiserver.database.model.project import Project
|
||||||
from apiserver.database.model.task.task import Task
|
|
||||||
from apiserver.database.utils import (
|
from apiserver.database.utils import (
|
||||||
parse_from_call,
|
parse_from_call,
|
||||||
get_company_or_none_constraint,
|
get_company_or_none_constraint,
|
||||||
@ -178,36 +179,21 @@ def update(call: APICall):
|
|||||||
call.result.data_model = UpdateResponse(updated=updated, fields=fields)
|
call.result.data_model = UpdateResponse(updated=updated, fields=fields)
|
||||||
|
|
||||||
|
|
||||||
@endpoint("projects.delete", required_fields=["project"])
|
def _reset_cached_tags(company: str, projects: Sequence[str]):
|
||||||
def delete(call):
|
org_bll.reset_tags(company, Tags.Task, projects=projects)
|
||||||
assert isinstance(call, APICall)
|
org_bll.reset_tags(company, Tags.Model, projects=projects)
|
||||||
project_id = call.data["project"]
|
|
||||||
force = call.data.get("force", False)
|
|
||||||
|
|
||||||
with translate_errors_context():
|
|
||||||
project = Project.get_for_writing(company=call.identity.company, id=project_id)
|
|
||||||
if not project:
|
|
||||||
raise errors.bad_request.InvalidProjectId(id=project_id)
|
|
||||||
|
|
||||||
# NOTE: from this point on we'll use the project ID and won't check for company, since we assume we already
|
@endpoint("projects.delete", request_data_model=DeleteRequest)
|
||||||
# have the correct project ID.
|
def delete(call: APICall, company_id: str, request: DeleteRequest):
|
||||||
|
res = delete_project(
|
||||||
# Find the tasks which belong to the project
|
company=company_id,
|
||||||
for cls, error in (
|
project_id=request.project,
|
||||||
(Task, errors.bad_request.ProjectHasTasks),
|
force=request.force,
|
||||||
(Model, errors.bad_request.ProjectHasModels),
|
delete_contents=request.delete_contents,
|
||||||
):
|
)
|
||||||
res = cls.objects(
|
_reset_cached_tags(company_id, projects=[request.project])
|
||||||
project=project_id, system_tags__nin=[EntityVisibility.archived.value]
|
call.result.data = {**attr.asdict(res)}
|
||||||
).only("id")
|
|
||||||
if res and not force:
|
|
||||||
raise error("use force=true to delete", id=project_id)
|
|
||||||
|
|
||||||
updated_count = res.update(project=None)
|
|
||||||
|
|
||||||
project.delete()
|
|
||||||
|
|
||||||
call.result.data = {"deleted": 1, "disassociated_tasks": updated_count}
|
|
||||||
|
|
||||||
|
|
||||||
@endpoint("projects.get_unique_metric_variants", request_data_model=ProjectReq)
|
@endpoint("projects.get_unique_metric_variants", request_data_model=ProjectReq)
|
||||||
|
@ -67,7 +67,7 @@ from apiserver.bll.task.param_utils import (
|
|||||||
escape_paths,
|
escape_paths,
|
||||||
)
|
)
|
||||||
from apiserver.bll.task.task_cleanup import cleanup_task
|
from apiserver.bll.task.task_cleanup import cleanup_task
|
||||||
from apiserver.bll.task.utils import update_task, task_deleted_prefix
|
from apiserver.bll.task.utils import update_task, deleted_prefix
|
||||||
from apiserver.bll.util import SetFieldsResolver
|
from apiserver.bll.util import SetFieldsResolver
|
||||||
from apiserver.database.errors import translate_errors_context
|
from apiserver.database.errors import translate_errors_context
|
||||||
from apiserver.database.model import EntityVisibility
|
from apiserver.database.model import EntityVisibility
|
||||||
@ -384,7 +384,7 @@ def _validate_and_get_task_from_call(call: APICall, **kwargs) -> Tuple[Task, dic
|
|||||||
@endpoint("tasks.validate", request_data_model=CreateRequest)
|
@endpoint("tasks.validate", request_data_model=CreateRequest)
|
||||||
def validate(call: APICall, company_id, req_model: CreateRequest):
|
def validate(call: APICall, company_id, req_model: CreateRequest):
|
||||||
parent = call.data.get("parent")
|
parent = call.data.get("parent")
|
||||||
if parent and parent.startswith(task_deleted_prefix):
|
if parent and parent.startswith(deleted_prefix):
|
||||||
call.data.pop("parent")
|
call.data.pop("parent")
|
||||||
_validate_and_get_task_from_call(call)
|
_validate_and_get_task_from_call(call)
|
||||||
|
|
||||||
@ -854,6 +854,7 @@ def reset(call: APICall, company_id, request: ResetRequest):
|
|||||||
force=force,
|
force=force,
|
||||||
update_children=False,
|
update_children=False,
|
||||||
return_file_urls=request.return_file_urls,
|
return_file_urls=request.return_file_urls,
|
||||||
|
delete_output_models=request.delete_output_models,
|
||||||
)
|
)
|
||||||
api_results.update(attr.asdict(cleaned_up))
|
api_results.update(attr.asdict(cleaned_up))
|
||||||
|
|
||||||
@ -943,13 +944,13 @@ def archive(call: APICall, company_id, request: ArchiveRequest):
|
|||||||
|
|
||||||
|
|
||||||
@endpoint("tasks.delete", request_data_model=DeleteRequest)
|
@endpoint("tasks.delete", request_data_model=DeleteRequest)
|
||||||
def delete(call: APICall, company_id, req_model: DeleteRequest):
|
def delete(call: APICall, company_id, request: DeleteRequest):
|
||||||
task = TaskBLL.get_task_with_access(
|
task = TaskBLL.get_task_with_access(
|
||||||
req_model.task, company_id=company_id, requires_write_access=True
|
request.task, company_id=company_id, requires_write_access=True
|
||||||
)
|
)
|
||||||
|
|
||||||
move_to_trash = req_model.move_to_trash
|
move_to_trash = request.move_to_trash
|
||||||
force = req_model.force
|
force = request.force
|
||||||
|
|
||||||
if task.status != TaskStatus.created and not force:
|
if task.status != TaskStatus.created and not force:
|
||||||
raise errors.bad_request.TaskCannotBeDeleted(
|
raise errors.bad_request.TaskCannotBeDeleted(
|
||||||
@ -961,7 +962,10 @@ def delete(call: APICall, company_id, req_model: DeleteRequest):
|
|||||||
|
|
||||||
with translate_errors_context():
|
with translate_errors_context():
|
||||||
result = cleanup_task(
|
result = cleanup_task(
|
||||||
task, force=force, return_file_urls=req_model.return_file_urls
|
task,
|
||||||
|
force=force,
|
||||||
|
return_file_urls=request.return_file_urls,
|
||||||
|
delete_output_models=request.delete_output_models,
|
||||||
)
|
)
|
||||||
|
|
||||||
if move_to_trash:
|
if move_to_trash:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Set
|
from typing import Set, Tuple
|
||||||
|
|
||||||
from apiserver.apierrors import errors
|
from apiserver.apierrors import errors
|
||||||
from apiserver.es_factory import es_factory
|
from apiserver.es_factory import es_factory
|
||||||
@ -7,7 +7,7 @@ from apiserver.tests.automated import TestService
|
|||||||
|
|
||||||
class TestTasksResetDelete(TestService):
|
class TestTasksResetDelete(TestService):
|
||||||
def setUp(self, **kwargs):
|
def setUp(self, **kwargs):
|
||||||
super().setUp(version="2.11")
|
super().setUp(version="2.13")
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
# draft task can be deleted
|
# draft task can be deleted
|
||||||
@ -50,12 +50,12 @@ class TestTasksResetDelete(TestService):
|
|||||||
self.assertEqual(res.urls.artifact_urls, [])
|
self.assertEqual(res.urls.artifact_urls, [])
|
||||||
|
|
||||||
task = self.new_task()
|
task = self.new_task()
|
||||||
model_urls = self.create_task_models(task)
|
published_model_urls, draft_model_urls = self.create_task_models(task)
|
||||||
artifact_urls = self.send_artifacts(task)
|
artifact_urls = self.send_artifacts(task)
|
||||||
event_urls = self.send_debug_image_events(task)
|
event_urls = self.send_debug_image_events(task)
|
||||||
event_urls.update(self.send_plot_events(task))
|
event_urls.update(self.send_plot_events(task))
|
||||||
res = self.assert_delete_task(task, force=True, return_file_urls=True)
|
res = self.assert_delete_task(task, force=True, return_file_urls=True)
|
||||||
self.assertEqual(set(res.urls.model_urls), model_urls)
|
self.assertEqual(set(res.urls.model_urls), draft_model_urls)
|
||||||
self.assertEqual(set(res.urls.event_urls), event_urls)
|
self.assertEqual(set(res.urls.event_urls), event_urls)
|
||||||
self.assertEqual(set(res.urls.artifact_urls), artifact_urls)
|
self.assertEqual(set(res.urls.artifact_urls), artifact_urls)
|
||||||
|
|
||||||
@ -73,21 +73,59 @@ class TestTasksResetDelete(TestService):
|
|||||||
self.api.tasks.reset(task=task, force=True)
|
self.api.tasks.reset(task=task, force=True)
|
||||||
|
|
||||||
# test urls
|
# test urls
|
||||||
task = self.new_task()
|
task, (published_model_urls, draft_model_urls), artifact_urls, event_urls = self.create_task_with_data()
|
||||||
model_urls = self.create_task_models(task)
|
|
||||||
artifact_urls = self.send_artifacts(task)
|
|
||||||
event_urls = self.send_debug_image_events(task)
|
|
||||||
event_urls.update(self.send_plot_events(task))
|
|
||||||
res = self.api.tasks.reset(task=task, force=True, return_file_urls=True)
|
res = self.api.tasks.reset(task=task, force=True, return_file_urls=True)
|
||||||
self.assertEqual(set(res.urls.model_urls), model_urls)
|
self.assertEqual(set(res.urls.model_urls), draft_model_urls)
|
||||||
self.assertEqual(set(res.urls.event_urls), event_urls)
|
self.assertEqual(set(res.urls.event_urls), event_urls)
|
||||||
self.assertEqual(set(res.urls.artifact_urls), artifact_urls)
|
self.assertEqual(set(res.urls.artifact_urls), artifact_urls)
|
||||||
|
|
||||||
def test_model_delete(self):
|
def test_model_delete(self):
|
||||||
model = self.new_model(uri="test")
|
model = self.new_model(uri="test")
|
||||||
res = self.api.models.delete(model=model, return_file_url=True)
|
res = self.api.models.delete(model=model)
|
||||||
self.assertEqual(res.url, "test")
|
self.assertEqual(res.url, "test")
|
||||||
|
|
||||||
|
def test_project_delete(self):
|
||||||
|
# without delete_contents flag
|
||||||
|
project = self.new_project()
|
||||||
|
task = self.new_task(project=project)
|
||||||
|
res = self.api.tasks.get_by_id(task=task)
|
||||||
|
self.assertEqual(res.task.get("project"), project)
|
||||||
|
|
||||||
|
res = self.api.projects.delete(project=project, force=True)
|
||||||
|
self.assertEqual(res.deleted, 1)
|
||||||
|
self.assertEqual(res.disassociated_tasks, 1)
|
||||||
|
self.assertEqual(res.deleted_tasks, 0)
|
||||||
|
res = self.api.tasks.get_by_id(task=task)
|
||||||
|
self.assertEqual(res.task.get("project"), None)
|
||||||
|
|
||||||
|
# with delete_contents flag
|
||||||
|
project = self.new_project()
|
||||||
|
task, (published_model_urls, draft_model_urls), artifact_urls, event_urls = self.create_task_with_data(
|
||||||
|
project=project
|
||||||
|
)
|
||||||
|
res = self.api.projects.delete(
|
||||||
|
project=project, force=True, delete_contents=True
|
||||||
|
)
|
||||||
|
self.assertEqual(set(res.urls.model_urls), published_model_urls | draft_model_urls)
|
||||||
|
self.assertEqual(res.deleted, 1)
|
||||||
|
self.assertEqual(res.disassociated_tasks, 0)
|
||||||
|
self.assertEqual(res.deleted_tasks, 1)
|
||||||
|
self.assertEqual(res.deleted_models, 2)
|
||||||
|
self.assertEqual(set(res.urls.event_urls), event_urls)
|
||||||
|
self.assertEqual(set(res.urls.artifact_urls), artifact_urls)
|
||||||
|
with self.api.raises(errors.bad_request.InvalidTaskId):
|
||||||
|
self.api.tasks.get_by_id(task=task)
|
||||||
|
|
||||||
|
def create_task_with_data(
|
||||||
|
self, **kwargs
|
||||||
|
) -> Tuple[str, Tuple[Set[str], Set[str]], Set[str], Set[str]]:
|
||||||
|
task = self.new_task(**kwargs)
|
||||||
|
published_model_urls, draft_model_urls = self.create_task_models(task, **kwargs)
|
||||||
|
artifact_urls = self.send_artifacts(task)
|
||||||
|
event_urls = self.send_debug_image_events(task)
|
||||||
|
event_urls.update(self.send_plot_events(task))
|
||||||
|
return task, (published_model_urls, draft_model_urls), artifact_urls, event_urls
|
||||||
|
|
||||||
def assert_delete_task(self, task_id, force=False, return_file_urls=False):
|
def assert_delete_task(self, task_id, force=False, return_file_urls=False):
|
||||||
tasks = self.api.tasks.get_all_ex(id=[task_id]).tasks
|
tasks = self.api.tasks.get_all_ex(id=[task_id]).tasks
|
||||||
self.assertEqual(tasks[0].id, task_id)
|
self.assertEqual(tasks[0].id, task_id)
|
||||||
@ -99,15 +137,15 @@ class TestTasksResetDelete(TestService):
|
|||||||
self.assertEqual(tasks, [])
|
self.assertEqual(tasks, [])
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def create_task_models(self, task) -> Set[str]:
|
def create_task_models(self, task, **kwargs) -> Tuple[Set[str], Set[str]]:
|
||||||
"""
|
"""
|
||||||
Update models from task and return only non public models
|
Update models from task and return only non public models
|
||||||
"""
|
"""
|
||||||
model_ready = self.new_model(uri="ready")
|
model_ready = self.new_model(uri="ready", **kwargs)
|
||||||
model_not_ready = self.new_model(uri="not_ready", ready=False)
|
model_not_ready = self.new_model(uri="not_ready", ready=False, **kwargs)
|
||||||
self.api.models.edit(model=model_not_ready, task=task)
|
self.api.models.edit(model=model_not_ready, task=task)
|
||||||
self.api.models.edit(model=model_ready, task=task)
|
self.api.models.edit(model=model_ready, task=task)
|
||||||
return {"not_ready"}
|
return {"ready"}, {"not_ready"}
|
||||||
|
|
||||||
def send_artifacts(self, task) -> Set[str]:
|
def send_artifacts(self, task) -> Set[str]:
|
||||||
"""
|
"""
|
||||||
@ -123,7 +161,9 @@ class TestTasksResetDelete(TestService):
|
|||||||
|
|
||||||
def send_debug_image_events(self, task) -> Set[str]:
|
def send_debug_image_events(self, task) -> Set[str]:
|
||||||
events = [
|
events = [
|
||||||
self.create_event(task, "training_debug_image", iteration, url=f"url_{iteration}")
|
self.create_event(
|
||||||
|
task, "training_debug_image", iteration, url=f"url_{iteration}"
|
||||||
|
)
|
||||||
for iteration in range(5)
|
for iteration in range(5)
|
||||||
]
|
]
|
||||||
self.send_batch(events)
|
self.send_batch(events)
|
||||||
@ -161,23 +201,22 @@ class TestTasksResetDelete(TestService):
|
|||||||
_, data = self.api.send_batch("events.add_batch", events)
|
_, data = self.api.send_batch("events.add_batch", events)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
name = "test task delete"
|
||||||
|
delete_params = dict(can_fail=True, force=True)
|
||||||
|
|
||||||
def new_task(self, **kwargs):
|
def new_task(self, **kwargs):
|
||||||
return self.create_temp(
|
self.update_missing(
|
||||||
"tasks",
|
kwargs, name=self.name, type="testing", input=dict(view=dict())
|
||||||
delete_params=dict(can_fail=True),
|
|
||||||
type="testing",
|
|
||||||
name="test task delete",
|
|
||||||
input=dict(view=dict()),
|
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
|
return self.create_temp("tasks", delete_params=self.delete_params, **kwargs,)
|
||||||
|
|
||||||
def new_model(self, **kwargs):
|
def new_model(self, **kwargs):
|
||||||
self.update_missing(kwargs, name="test", uri="file:///a/b", labels={})
|
self.update_missing(kwargs, name=self.name, uri="file:///a/b", labels={})
|
||||||
return self.create_temp(
|
return self.create_temp("models", delete_params=self.delete_params, **kwargs,)
|
||||||
"models",
|
|
||||||
delete_params=dict(can_fail=True),
|
def new_project(self, **kwargs):
|
||||||
**kwargs,
|
self.update_missing(kwargs, name=self.name, description="")
|
||||||
)
|
return self.create_temp("projects", delete_params=self.delete_params, **kwargs)
|
||||||
|
|
||||||
def publish_task(self, task_id):
|
def publish_task(self, task_id):
|
||||||
self.api.tasks.started(task=task_id)
|
self.api.tasks.started(task=task_id)
|
||||||
|
Loading…
Reference in New Issue
Block a user