mirror of
https://github.com/clearml/clearml-server
synced 2025-06-26 23:15:47 +00:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4684fd5b74 | ||
|
|
e08123fcc0 | ||
|
|
e713e876eb | ||
|
|
c2cc788319 | ||
|
|
da8315d0db | ||
|
|
4ac6f88278 | ||
|
|
a7865ccbec | ||
|
|
ec14f327c6 | ||
|
|
a03b24d6b6 | ||
|
|
cb71ef8e47 | ||
|
|
8678fbc995 | ||
|
|
58df8f201a | ||
|
|
f4bf16c156 | ||
|
|
942f996237 | ||
|
|
c1e7f8f9c1 | ||
|
|
274c487b37 | ||
|
|
cc0129a800 | ||
|
|
388dd1b01f | ||
|
|
d62ecb5e6e | ||
|
|
6d507616b3 | ||
|
|
d0252a6dd9 | ||
|
|
2263e7cc1e | ||
|
|
81b93e6811 | ||
|
|
491e83d0f1 | ||
|
|
f84cc0a2cb | ||
|
|
6c5f966ed4 | ||
|
|
4eff657810 | ||
|
|
74acaa31df | ||
|
|
21ed8559bf | ||
|
|
3927604648 | ||
|
|
f7dcbd96ec | ||
|
|
5950b81f0b | ||
|
|
1e51e2e221 | ||
|
|
4c98b87554 | ||
|
|
c196043d2a | ||
|
|
752020c66a | ||
|
|
6885d07462 | ||
|
|
00552da1b0 | ||
|
|
eebe2eeffc | ||
|
|
bc2fe28bdd | ||
|
|
ed86750b24 | ||
|
|
6df69afb25 | ||
|
|
3f22423c3f | ||
|
|
3ad636c468 | ||
|
|
5c80336aa9 | ||
|
|
5cd59ea6e3 | ||
|
|
5d3ba4fa73 | ||
|
|
42556c8dbb | ||
|
|
dbe1c6f00f | ||
|
|
a17485b1bd | ||
|
|
a2b9fed92d | ||
|
|
ff34da3c88 | ||
|
|
5239755066 | ||
|
|
8061dfedbb | ||
|
|
011164ce9b | ||
|
|
8135cf5258 | ||
|
|
a83a932e84 | ||
|
|
db021f2863 | ||
|
|
1b650b1689 | ||
|
|
14d18a7aba | ||
|
|
a7ed46979f | ||
|
|
452f606889 | ||
|
|
fc47ccbf09 | ||
|
|
0206811342 | ||
|
|
a3ac1049a3 | ||
|
|
8488f63a3a | ||
|
|
9206a7c57d | ||
|
|
0c37ced2a1 | ||
|
|
b22f26129e | ||
|
|
d8b998ebd8 | ||
|
|
741fa84b52 | ||
|
|
d9579891c8 | ||
|
|
900414d0de | ||
|
|
5449b332d2 | ||
|
|
875f4b9536 | ||
|
|
95b8f22899 | ||
|
|
4058fb9ce5 | ||
|
|
cf8e847ed3 | ||
|
|
755cc803d9 | ||
|
|
3729afe014 | ||
|
|
dff2ed34e8 | ||
|
|
de9651d761 | ||
|
|
818496236b | ||
|
|
e99817b28b | ||
|
|
58465fbc17 | ||
|
|
2e4e060a82 | ||
|
|
5c5d9b6434 | ||
|
|
4291ad682a | ||
|
|
4c22757002 | ||
|
|
6e777e80b8 | ||
|
|
c8e4d9eeac | ||
|
|
b51aa5c29b | ||
|
|
e7c9daa42b | ||
|
|
7357654249 | ||
|
|
a6f671b46a | ||
|
|
17a8b440bd | ||
|
|
eb2b9cbd9a | ||
|
|
797e503e67 | ||
|
|
30cfdac8f2 | ||
|
|
24bb87aaee | ||
|
|
dd49ba180a | ||
|
|
bda903d0d8 | ||
|
|
9739eb2d5a | ||
|
|
cfbb37238f | ||
|
|
6664c6237e | ||
|
|
74200a24bd | ||
|
|
2fb9288a6c | ||
|
|
5d014d81af | ||
|
|
3a2675abe1 | ||
|
|
f0d68b1ce9 | ||
|
|
15db9cdaef | ||
|
|
a45d47f5d7 | ||
|
|
b1a50c1370 | ||
|
|
22a2a02760 | ||
|
|
ab798e4170 | ||
|
|
f09ac672d2 | ||
|
|
2149b76f63 | ||
|
|
d96420aa67 | ||
|
|
ed6c7b7bcb | ||
|
|
a392bc0bd7 | ||
|
|
7e97ec5555 | ||
|
|
9c41124b81 | ||
|
|
14ff639bb0 | ||
|
|
e66257761a | ||
|
|
0ffde24dc2 | ||
|
|
d4fdcd9b32 | ||
|
|
18570bfccb | ||
|
|
54ce6c34c6 | ||
|
|
ae4c33fa0e | ||
|
|
c7cd949fd0 | ||
|
|
1ce4058157 |
@@ -27,7 +27,7 @@
|
||||
24: ["not_public_object", "object is not public"]
|
||||
|
||||
# Auth / Login
|
||||
75: ["invalid_access_key", "access key not found for user"]
|
||||
75: ["invalid_access_key", "access key not found"]
|
||||
|
||||
# Tasks
|
||||
100: ["task_error", "general task error"]
|
||||
@@ -50,6 +50,12 @@
|
||||
130: ["task_not_found", "task not found"]
|
||||
131: ["events_not_added", "events not added"]
|
||||
|
||||
# Reports
|
||||
150: ["operation_supported_on_reports_only", "passed task is not report"]
|
||||
|
||||
# Pipelines
|
||||
160: ["cannot_remove_all_runs", "at least one pipeline run should be left"]
|
||||
|
||||
# Models
|
||||
200: ["model_error", "general task error"]
|
||||
201: ["invalid_model_id", "invalid model id"]
|
||||
@@ -70,12 +76,14 @@
|
||||
402: ["project_has_tasks", "project has associated tasks"]
|
||||
403: ["project_not_found", "project not found"]
|
||||
405: ["project_has_models", "project has associated models"]
|
||||
406: ["project_has_datasets", "project has associated non-empty datasets"]
|
||||
407: ["invalid_project_name", "invalid project name"]
|
||||
408: ["cannot_update_project_location", "Cannot update project location. Use projects.move instead"]
|
||||
409: ["project_path_exceeds_max", "Project path exceed the maximum allowed depth"]
|
||||
410: ["project_source_and_destination_are_the_same", "Project has the same source and destination paths"]
|
||||
411: ["project_cannot_be_moved_under_itself", "Project can not be moved under itself in the projects hierarchy"]
|
||||
412: ["project_cannot_be_merged_into_its_child", "Project can not be merged into its own child"]
|
||||
413: ["project_has_pipelines", "project has associated pipelines with active controllers"]
|
||||
|
||||
# Queues
|
||||
701: ["invalid_queue_id", "invalid queue id"]
|
||||
|
||||
@@ -61,6 +61,13 @@ class ListField(fields.ListField):
|
||||
item.validate()
|
||||
|
||||
|
||||
class ScalarField(fields.BaseField):
|
||||
|
||||
"""String field."""
|
||||
|
||||
types = (str, int, float, bool)
|
||||
|
||||
|
||||
class DictField(fields.BaseField):
|
||||
types = (dict,)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class MultiTaskScalarMetricsIterHistogramRequest(HistogramRequestBase):
|
||||
Length(
|
||||
minimum_value=1,
|
||||
maximum_value=config.get(
|
||||
"services.tasks.multi_task_histogram_limit", 10
|
||||
"services.tasks.multi_task_histogram_limit", 100
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -155,6 +155,13 @@ class TaskMetricsRequest(MultiTasksRequestBase):
|
||||
event_type: EventType = ActualEnumField(EventType, required=True)
|
||||
|
||||
|
||||
class MultiTaskPlotsRequest(MultiTasksRequestBase):
|
||||
iters: int = IntField(default=1)
|
||||
scroll_id: str = StringField()
|
||||
no_scroll: bool = BoolField(default=False)
|
||||
last_iters_per_task_metric: bool = BoolField(default=True)
|
||||
|
||||
|
||||
class TaskPlotsRequest(Base):
|
||||
task: str = StringField(required=True)
|
||||
iters: int = IntField(default=1)
|
||||
|
||||
@@ -44,10 +44,12 @@ class ModelRequest(models.Base):
|
||||
|
||||
class DeleteModelRequest(ModelRequest):
|
||||
force = fields.BoolField(default=False)
|
||||
delete_external_artifacts = fields.BoolField(default=True)
|
||||
|
||||
|
||||
class ModelsDeleteManyRequest(BatchRequest):
|
||||
force = fields.BoolField(default=False)
|
||||
delete_external_artifacts = fields.BoolField(default=True)
|
||||
|
||||
|
||||
class PublishModelRequest(ModelRequest):
|
||||
@@ -79,3 +81,4 @@ class AddOrUpdateMetadataRequest(AddOrUpdateMetadata):
|
||||
|
||||
class ModelsGetRequest(models.Base):
|
||||
include_stats = fields.BoolField(default=False)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from jsonmodels import fields, models
|
||||
from enum import auto
|
||||
from typing import Sequence
|
||||
|
||||
from apiserver.apimodels import DictField
|
||||
from jsonmodels import fields, models
|
||||
from jsonmodels.validators import Length
|
||||
|
||||
from apiserver.apimodels import DictField, ActualEnumField, ScalarField
|
||||
from apiserver.utilities.stringenum import StringEnum
|
||||
|
||||
|
||||
class Filter(models.Base):
|
||||
@@ -19,5 +24,39 @@ class EntitiesCountRequest(models.Base):
|
||||
models = DictField()
|
||||
pipelines = DictField()
|
||||
datasets = DictField()
|
||||
reports = DictField()
|
||||
active_users = fields.ListField(str)
|
||||
search_hidden = fields.BoolField(default=False)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
|
||||
|
||||
class EntityType(StringEnum):
|
||||
task = auto()
|
||||
model = auto()
|
||||
|
||||
|
||||
class ValueMapping(models.Base):
|
||||
key = ScalarField(nullable=True)
|
||||
value = ScalarField(nullable=True)
|
||||
|
||||
|
||||
class FieldMapping(models.Base):
|
||||
field = fields.StringField(required=True)
|
||||
name = fields.StringField()
|
||||
values: Sequence[ValueMapping] = fields.ListField(items_types=[ValueMapping])
|
||||
|
||||
|
||||
class PrepareDownloadForGetAllRequest(models.Base):
|
||||
entity_type = ActualEnumField(EntityType)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
search_hidden = fields.BoolField(default=False)
|
||||
only_fields = fields.ListField(
|
||||
items_types=[str], validators=[Length(1)], required=True
|
||||
)
|
||||
field_mappings: Sequence[FieldMapping] = fields.ListField(
|
||||
items_types=[FieldMapping], validators=[Length(1)], required=True
|
||||
)
|
||||
|
||||
|
||||
class DownloadForGetAllRequest(models.Base):
|
||||
prepare_id = fields.StringField(required=True)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from jsonmodels import models, fields
|
||||
from jsonmodels.validators import Length
|
||||
|
||||
from apiserver.apimodels import ListField
|
||||
|
||||
@@ -8,6 +9,11 @@ class Arg(models.Base):
|
||||
value = fields.StringField(required=True)
|
||||
|
||||
|
||||
class DeleteRunsRequest(models.Base):
|
||||
project = fields.StringField(required=True)
|
||||
ids = ListField([str], required=True, validators=[Length(1)])
|
||||
|
||||
|
||||
class StartPipelineRequest(models.Base):
|
||||
task = fields.StringField(required=True)
|
||||
queue = fields.StringField(required=True)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from enum import Enum, auto
|
||||
|
||||
from jsonmodels import models, fields
|
||||
|
||||
from apiserver.apimodels import ListField, ActualEnumField, DictField
|
||||
from apiserver.apimodels.organization import TagsRequest
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.utilities.stringenum import StringEnum
|
||||
|
||||
|
||||
class ProjectRequest(models.Base):
|
||||
@@ -20,6 +23,7 @@ class MoveRequest(ProjectRequest):
|
||||
class DeleteRequest(ProjectRequest):
|
||||
force = fields.BoolField(default=False)
|
||||
delete_contents = fields.BoolField(default=False)
|
||||
delete_external_artifacts = fields.BoolField(default=True)
|
||||
|
||||
|
||||
class ProjectOrNoneRequest(models.Base):
|
||||
@@ -27,6 +31,10 @@ class ProjectOrNoneRequest(models.Base):
|
||||
include_subprojects = fields.BoolField(default=True)
|
||||
|
||||
|
||||
class GetUniqueMetricsRequest(ProjectOrNoneRequest):
|
||||
model_metrics = fields.BoolField(default=False)
|
||||
|
||||
|
||||
class GetParamsRequest(ProjectOrNoneRequest):
|
||||
page = fields.IntField(default=0)
|
||||
page_size = fields.IntField(default=500)
|
||||
@@ -43,17 +51,38 @@ class MultiProjectRequest(models.Base):
|
||||
|
||||
class ProjectTaskParentsRequest(MultiProjectRequest):
|
||||
tasks_state = ActualEnumField(EntityVisibility)
|
||||
task_name = fields.StringField()
|
||||
|
||||
|
||||
class ProjectHyperparamValuesRequest(MultiProjectRequest):
|
||||
class EntityTypeEnum(StringEnum):
|
||||
task = auto()
|
||||
model = auto()
|
||||
|
||||
|
||||
class ProjectUserNamesRequest(MultiProjectRequest):
|
||||
entity = ActualEnumField(EntityTypeEnum, default=EntityTypeEnum.task)
|
||||
|
||||
|
||||
class MultiProjectPagedRequest(MultiProjectRequest):
|
||||
allow_public = fields.BoolField(default=True)
|
||||
page = fields.IntField(default=0)
|
||||
page_size = fields.IntField(default=500)
|
||||
|
||||
|
||||
class ProjectHyperparamValuesRequest(MultiProjectPagedRequest):
|
||||
section = fields.StringField(required=True)
|
||||
name = fields.StringField(required=True)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
pattern = fields.StringField()
|
||||
|
||||
|
||||
class ProjectModelMetadataValuesRequest(MultiProjectRequest):
|
||||
class ProjectModelMetadataValuesRequest(MultiProjectPagedRequest):
|
||||
key = fields.StringField(required=True)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
|
||||
|
||||
class ProjectChildrenType(Enum):
|
||||
pipeline = "pipeline"
|
||||
report = "report"
|
||||
dataset = "dataset"
|
||||
|
||||
|
||||
class ProjectsGetRequest(models.Base):
|
||||
@@ -62,8 +91,12 @@ class ProjectsGetRequest(models.Base):
|
||||
include_stats_filter = DictField()
|
||||
stats_with_children = fields.BoolField(default=True)
|
||||
stats_for_state = ActualEnumField(EntityVisibility, default=EntityVisibility.active)
|
||||
non_public = fields.BoolField(default=False)
|
||||
non_public = fields.BoolField(default=False) # legacy, use allow_public instead
|
||||
active_users = fields.ListField(str)
|
||||
check_own_contents = fields.BoolField(default=False)
|
||||
shallow_search = fields.BoolField(default=False)
|
||||
search_hidden = fields.BoolField(default=False)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
children_type = ActualEnumField(ProjectChildrenType)
|
||||
children_tags = fields.ListField(str)
|
||||
children_tags_filter = DictField()
|
||||
|
||||
84
apiserver/apimodels/reports.py
Normal file
84
apiserver/apimodels/reports.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from typing import Sequence
|
||||
|
||||
from jsonmodels import validators
|
||||
from jsonmodels.fields import StringField, ListField, BoolField, EmbeddedField, IntField
|
||||
from jsonmodels.models import Base
|
||||
from jsonmodels.validators import Length
|
||||
|
||||
from apiserver.apimodels.events import MetricVariants, HistogramRequestBase
|
||||
|
||||
|
||||
class UpdateReportRequest(Base):
|
||||
task = StringField(required=True)
|
||||
name = StringField(nullable=True, validators=Length(minimum_value=3))
|
||||
tags = ListField(items_types=[str])
|
||||
comment = StringField()
|
||||
report = StringField()
|
||||
report_assets = ListField(items_types=[str])
|
||||
|
||||
|
||||
class CreateReportRequest(Base):
|
||||
name = StringField(required=True, validators=Length(minimum_value=3))
|
||||
tags = ListField(items_types=[str])
|
||||
comment = StringField()
|
||||
report = StringField()
|
||||
project = StringField()
|
||||
report_assets = ListField(items_types=[str])
|
||||
|
||||
|
||||
class PublishReportRequest(Base):
|
||||
task = StringField(required=True)
|
||||
message = StringField(default="")
|
||||
|
||||
|
||||
class ArchiveReportRequest(Base):
|
||||
task = StringField(required=True)
|
||||
message = StringField(default="")
|
||||
|
||||
|
||||
class ShareReportRequest(Base):
|
||||
task = StringField(required=True)
|
||||
share = BoolField(default=True)
|
||||
|
||||
|
||||
class DeleteReportRequest(Base):
|
||||
task = StringField(required=True)
|
||||
force = BoolField(default=False)
|
||||
|
||||
|
||||
class MoveReportRequest(Base):
|
||||
task = StringField(required=True)
|
||||
project = StringField()
|
||||
project_name = StringField()
|
||||
|
||||
|
||||
class EventsRequest(Base):
|
||||
iters = IntField(default=1, validators=validators.Min(1))
|
||||
metrics: Sequence[MetricVariants] = ListField(items_types=MetricVariants)
|
||||
|
||||
|
||||
class PlotEventsRequest(EventsRequest):
|
||||
last_iters_per_task_metric: bool = BoolField(default=True)
|
||||
|
||||
|
||||
class ScalarMetricsIterHistogram(HistogramRequestBase):
|
||||
metrics: Sequence[MetricVariants] = ListField(items_types=MetricVariants)
|
||||
|
||||
|
||||
class SingleValueMetrics(Base):
|
||||
pass
|
||||
|
||||
|
||||
class GetTasksDataRequest(Base):
|
||||
debug_images: EventsRequest = EmbeddedField(EventsRequest)
|
||||
plots: PlotEventsRequest = EmbeddedField(PlotEventsRequest)
|
||||
scalar_metrics_iter_histogram: ScalarMetricsIterHistogram = EmbeddedField(
|
||||
ScalarMetricsIterHistogram
|
||||
)
|
||||
single_value_metrics: SingleValueMetrics = EmbeddedField(SingleValueMetrics)
|
||||
allow_public = BoolField(default=True)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class GetAllRequest(Base):
|
||||
allow_public = BoolField(default=True)
|
||||
@@ -96,6 +96,11 @@ class UpdateRequest(TaskUpdateRequest):
|
||||
status_message = StringField(default="")
|
||||
|
||||
|
||||
class DequeueRequest(UpdateRequest):
|
||||
remove_from_all_queues = BoolField(default=False)
|
||||
new_status = StringField()
|
||||
|
||||
|
||||
class EnqueueRequest(UpdateRequest):
|
||||
queue = StringField()
|
||||
queue_name = StringField()
|
||||
@@ -274,6 +279,11 @@ class StopManyRequest(TaskBatchRequest):
|
||||
force = BoolField(default=False)
|
||||
|
||||
|
||||
class DequeueManyRequest(TaskBatchRequest):
|
||||
remove_from_all_queues = BoolField(default=False)
|
||||
new_status = StringField()
|
||||
|
||||
|
||||
class EnqueueManyRequest(TaskBatchRequest):
|
||||
queue = StringField()
|
||||
queue_name = StringField()
|
||||
@@ -318,3 +328,13 @@ class DeleteModelsRequest(TaskRequest):
|
||||
models: Sequence[ModelItemKey] = ListField(
|
||||
[ModelItemKey], validators=Length(minimum_value=1)
|
||||
)
|
||||
|
||||
|
||||
class GetAllReq(models.Base):
|
||||
allow_public = BoolField(default=True)
|
||||
search_hidden = BoolField(default=False)
|
||||
|
||||
|
||||
class UpdateTagsRequest(BatchRequest):
|
||||
add_tags = ListField([str])
|
||||
remove_tags = ListField([str])
|
||||
|
||||
@@ -12,9 +12,8 @@ from jsonmodels.fields import (
|
||||
)
|
||||
from jsonmodels.models import Base
|
||||
|
||||
from apiserver.apimodels import make_default, ListField, EnumField, JsonSerializableMixin
|
||||
|
||||
DEFAULT_TIMEOUT = 10 * 60
|
||||
from apiserver.apimodels import ListField, EnumField, JsonSerializableMixin
|
||||
from apiserver.config_repo import config
|
||||
|
||||
|
||||
class WorkerRequest(Base):
|
||||
@@ -24,9 +23,10 @@ class WorkerRequest(Base):
|
||||
|
||||
|
||||
class RegisterRequest(WorkerRequest):
|
||||
timeout = make_default(
|
||||
IntField, DEFAULT_TIMEOUT
|
||||
)() # registration timeout in seconds (default is 10min)
|
||||
timeout = IntField(
|
||||
default=int(config.get("services.workers.default_worker_timeout_sec", 10 * 60))
|
||||
)
|
||||
""" registration timeout in seconds (default is 10min) """
|
||||
queues = ListField(six.string_types) # list of queues this worker listens to
|
||||
|
||||
|
||||
@@ -106,6 +106,10 @@ class GetAllResponse(Base):
|
||||
workers = ListField(WorkerResponseEntry)
|
||||
|
||||
|
||||
class GetCountRequest(GetAllRequest):
|
||||
last_seen = IntField(default=0)
|
||||
|
||||
|
||||
class StatsBase(Base):
|
||||
worker_ids = ListField(str)
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import zlib
|
||||
from collections import defaultdict
|
||||
from contextlib import closing
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
from typing import Sequence, Set, Tuple, Optional, List, Mapping, Union
|
||||
|
||||
import elasticsearch
|
||||
from boltons.iterutils import chunked_iter
|
||||
from elasticsearch.helpers import BulkIndexError
|
||||
from mongoengine import Q
|
||||
from nested_dict import nested_dict
|
||||
@@ -23,12 +23,14 @@ from apiserver.bll.event.event_common import (
|
||||
get_metric_variants_condition,
|
||||
uncompress_plot,
|
||||
get_max_metric_and_variant_counts,
|
||||
PlotFields,
|
||||
)
|
||||
from apiserver.bll.event.events_iterator import EventsIterator, TaskEventsResult
|
||||
from apiserver.bll.event.history_debug_image_iterator import HistoryDebugImageIterator
|
||||
from apiserver.bll.event.history_plots_iterator import HistoryPlotsIterator
|
||||
from apiserver.bll.event.metric_debug_images_iterator import MetricDebugImagesIterator
|
||||
from apiserver.bll.event.metric_plots_iterator import MetricPlotsIterator
|
||||
from apiserver.bll.model import ModelBLL
|
||||
from apiserver.bll.util import parallel_chunked_decorator
|
||||
from apiserver.database import utils as dbutils
|
||||
from apiserver.database.model.model import Model
|
||||
@@ -45,25 +47,19 @@ from apiserver.utilities.dicts import nested_get
|
||||
from apiserver.utilities.json import loads
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
EVENT_TYPES: Set[str] = set(map(attrgetter("value"), EventType))
|
||||
EVENT_TYPES: Set[str] = set(et.value for et in EventType if et != EventType.all)
|
||||
LOCKED_TASK_STATUSES = (TaskStatus.publishing, TaskStatus.published)
|
||||
MAX_LONG = 2 ** 63 - 1
|
||||
MIN_LONG = -(2 ** 63)
|
||||
MAX_LONG = 2**63 - 1
|
||||
MIN_LONG = -(2**63)
|
||||
|
||||
|
||||
log = config.logger(__file__)
|
||||
|
||||
|
||||
class PlotFields:
|
||||
valid_plot = "valid_plot"
|
||||
plot_len = "plot_len"
|
||||
plot_str = "plot_str"
|
||||
plot_data = "plot_data"
|
||||
source_urls = "source_urls"
|
||||
async_task_events_delete = config.get("services.tasks.async_events_delete", False)
|
||||
async_delete_threshold = config.get("services.tasks.async_events_delete_threshold", 100_000)
|
||||
|
||||
|
||||
class EventBLL(object):
|
||||
id_fields = ("task", "iter", "metric", "variant", "key")
|
||||
event_id_fields = ("task", "iter", "metric", "variant", "key")
|
||||
empty_scroll = "FFFF"
|
||||
img_source_regex = re.compile(
|
||||
r"['\"]source['\"]:\s?['\"]([a-z][a-z0-9+\-.]*://.*?)['\"]",
|
||||
@@ -101,47 +97,76 @@ class EventBLL(object):
|
||||
return self._metrics
|
||||
|
||||
@staticmethod
|
||||
def _get_valid_tasks(company_id, task_ids: Set, allow_locked_tasks=False) -> Set:
|
||||
"""Verify that task exists and can be updated"""
|
||||
if not task_ids:
|
||||
def _get_valid_entities(company_id, ids: Mapping[str, bool], model=False) -> Set:
|
||||
"""Verify that task or model exists and can be updated"""
|
||||
if not ids:
|
||||
return set()
|
||||
|
||||
with translate_errors_context():
|
||||
query = Q(id__in=task_ids, company=company_id)
|
||||
if not allow_locked_tasks:
|
||||
query &= Q(status__nin=LOCKED_TASK_STATUSES)
|
||||
res = Task.objects(query).only("id")
|
||||
return {r.id for r in res}
|
||||
allow_locked = {id_ for id_, allowed in ids.items() if allowed}
|
||||
not_locked = {id_ for id_, allowed in ids.items() if not allowed}
|
||||
res = set()
|
||||
allow_locked_q = Q()
|
||||
not_locked_q = (
|
||||
Q(ready__ne=True) if model else Q(status__nin=LOCKED_TASK_STATUSES)
|
||||
)
|
||||
for requested_ids, locked_q in (
|
||||
(allow_locked, allow_locked_q),
|
||||
(not_locked, not_locked_q),
|
||||
):
|
||||
if not requested_ids:
|
||||
continue
|
||||
query = Q(id__in=requested_ids, company=company_id)
|
||||
res.update(
|
||||
(Model if model else Task).objects(query & locked_q).scalar("id")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_valid_models(company_id, model_ids: Set, allow_locked_models=False) -> Set:
|
||||
"""Verify that task exists and can be updated"""
|
||||
if not model_ids:
|
||||
return set()
|
||||
|
||||
with translate_errors_context():
|
||||
query = Q(id__in=model_ids, company=company_id)
|
||||
if not allow_locked_models:
|
||||
query &= Q(ready__ne=True)
|
||||
res = Model.objects(query).only("id")
|
||||
return {r.id for r in res}
|
||||
return res
|
||||
|
||||
def add_events(
|
||||
self, company_id, events, worker, allow_locked=False
|
||||
self,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
events: Sequence[dict],
|
||||
worker: str,
|
||||
) -> Tuple[int, int, dict]:
|
||||
model_events = events[0].get("model_event", False)
|
||||
task_ids = {}
|
||||
model_ids = {}
|
||||
for event in events:
|
||||
if event.get("model_event", model_events) != model_events:
|
||||
if event.get("model_event", False):
|
||||
model = event.pop("model", None)
|
||||
if model is not None:
|
||||
event["task"] = model
|
||||
entity_ids = model_ids
|
||||
else:
|
||||
event["model_event"] = False
|
||||
entity_ids = task_ids
|
||||
|
||||
id_ = event.get("task")
|
||||
allow_locked = event.pop("allow_locked", False)
|
||||
if not id_:
|
||||
continue
|
||||
|
||||
allowed_for_entity = entity_ids.get(id_)
|
||||
if allowed_for_entity is None:
|
||||
entity_ids[id_] = allow_locked
|
||||
elif allowed_for_entity != allow_locked:
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Inconsistent model_event setting in the passed events"
|
||||
)
|
||||
if event.pop("allow_locked", allow_locked) != allow_locked:
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Inconsistent allow_locked setting in the passed events"
|
||||
f"Inconsistent allow_locked setting in the passed events for {id_}"
|
||||
)
|
||||
|
||||
found_in_both = set(task_ids).intersection(set(model_ids))
|
||||
if found_in_both:
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Inconsistent model_event setting in the passed events",
|
||||
tasks=found_in_both,
|
||||
)
|
||||
valid_models = self._get_valid_entities(company_id, ids=model_ids, model=True)
|
||||
valid_tasks = self._get_valid_entities(company_id, ids=task_ids)
|
||||
|
||||
actions: List[dict] = []
|
||||
task_or_model_ids = set()
|
||||
used_task_ids = set()
|
||||
used_model_ids = set()
|
||||
task_iteration = defaultdict(lambda: 0)
|
||||
task_last_scalar_events = nested_dict(
|
||||
3, dict
|
||||
@@ -151,28 +176,6 @@ class EventBLL(object):
|
||||
) # task_id -> metric_hash -> event_type -> MetricEvent
|
||||
errors_per_type = defaultdict(int)
|
||||
invalid_iteration_error = f"Iteration number should not exceed {MAX_LONG}"
|
||||
if model_events:
|
||||
for event in events:
|
||||
model = event.pop("model", None)
|
||||
if model is not None:
|
||||
event["task"] = model
|
||||
valid_entities = self._get_valid_models(
|
||||
company_id,
|
||||
model_ids={
|
||||
event["task"] for event in events if event.get("task") is not None
|
||||
},
|
||||
allow_locked_models=allow_locked,
|
||||
)
|
||||
entity_name = "model"
|
||||
else:
|
||||
valid_entities = self._get_valid_tasks(
|
||||
company_id,
|
||||
task_ids={
|
||||
event["task"] for event in events if event.get("task") is not None
|
||||
},
|
||||
allow_locked_tasks=allow_locked,
|
||||
)
|
||||
entity_name = "task"
|
||||
|
||||
for event in events:
|
||||
# remove spaces from event type
|
||||
@@ -186,7 +189,8 @@ class EventBLL(object):
|
||||
errors_per_type[f"Invalid event type {event_type}"] += 1
|
||||
continue
|
||||
|
||||
if model_events and event_type == EventType.task_log.value:
|
||||
model_event = event["model_event"]
|
||||
if model_event and event_type == EventType.task_log.value:
|
||||
errors_per_type[f"Task log events are not supported for models"] += 1
|
||||
continue
|
||||
|
||||
@@ -195,8 +199,12 @@ class EventBLL(object):
|
||||
errors_per_type["Event must have a 'task' field"] += 1
|
||||
continue
|
||||
|
||||
if task_or_model_id not in valid_entities:
|
||||
errors_per_type[f"Invalid {entity_name} id {task_or_model_id}"] += 1
|
||||
if (model_event and task_or_model_id not in valid_models) or (
|
||||
not model_event and task_or_model_id not in valid_tasks
|
||||
):
|
||||
errors_per_type[
|
||||
f"Invalid {'model' if model_event else 'task'} id {task_or_model_id}"
|
||||
] += 1
|
||||
continue
|
||||
|
||||
event["type"] = event_type
|
||||
@@ -218,13 +226,10 @@ class EventBLL(object):
|
||||
# force iter to be a long int
|
||||
iter = event.get("iter")
|
||||
if iter is not None:
|
||||
if model_events:
|
||||
iter = 0
|
||||
else:
|
||||
iter = int(iter)
|
||||
if iter > MAX_LONG or iter < MIN_LONG:
|
||||
errors_per_type[invalid_iteration_error] += 1
|
||||
continue
|
||||
iter = int(iter)
|
||||
if iter > MAX_LONG or iter < MIN_LONG:
|
||||
errors_per_type[invalid_iteration_error] += 1
|
||||
continue
|
||||
event["iter"] = iter
|
||||
|
||||
# used to have "values" to indicate array. no need anymore
|
||||
@@ -234,7 +239,6 @@ class EventBLL(object):
|
||||
|
||||
event["metric"] = event.get("metric") or ""
|
||||
event["variant"] = event.get("variant") or ""
|
||||
event["model_event"] = model_events
|
||||
|
||||
index_name = get_index_name(company_id, event_type)
|
||||
es_action = {
|
||||
@@ -243,31 +247,33 @@ class EventBLL(object):
|
||||
"_source": event,
|
||||
}
|
||||
|
||||
# for "log" events, don't assing custom _id - whatever is sent, is written (not overwritten)
|
||||
# for "log" events, don't assign custom _id - whatever is sent, is written (not overwritten)
|
||||
if event_type != EventType.task_log.value:
|
||||
es_action["_id"] = self._get_event_id(event)
|
||||
else:
|
||||
es_action["_id"] = dbutils.id()
|
||||
|
||||
task_or_model_ids.add(task_or_model_id)
|
||||
if (
|
||||
iter is not None
|
||||
and not model_events
|
||||
and event.get("metric") not in self._skip_iteration_for_metric
|
||||
):
|
||||
task_iteration[task_or_model_id] = max(
|
||||
iter, task_iteration[task_or_model_id]
|
||||
)
|
||||
|
||||
if not model_events:
|
||||
if model_event:
|
||||
used_model_ids.add(task_or_model_id)
|
||||
else:
|
||||
used_task_ids.add(task_or_model_id)
|
||||
self._update_last_metric_events_for_task(
|
||||
last_events=task_last_events[task_or_model_id], event=event,
|
||||
last_events=task_last_events[task_or_model_id],
|
||||
event=event,
|
||||
)
|
||||
if event_type == EventType.metrics_scalar.value:
|
||||
self._update_last_scalar_events_for_task(
|
||||
last_events=task_last_scalar_events[task_or_model_id],
|
||||
event=event,
|
||||
)
|
||||
if event_type == EventType.metrics_scalar.value:
|
||||
self._update_last_scalar_events_for_task(
|
||||
last_events=task_last_scalar_events[task_or_model_id],
|
||||
event=event,
|
||||
)
|
||||
|
||||
actions.append(es_action)
|
||||
|
||||
@@ -305,31 +311,40 @@ class EventBLL(object):
|
||||
else:
|
||||
errors_per_type["Error when indexing events batch"] += 1
|
||||
|
||||
if not model_events:
|
||||
remaining_tasks = set()
|
||||
now = datetime.utcnow()
|
||||
for task_or_model_id in task_or_model_ids:
|
||||
# Update related tasks. For reasons of performance, we prefer to update
|
||||
# all of them and not only those who's events were successful
|
||||
updated = self._update_task(
|
||||
company_id=company_id,
|
||||
task_id=task_or_model_id,
|
||||
now=now,
|
||||
iter_max=task_iteration.get(task_or_model_id),
|
||||
last_scalar_events=task_last_scalar_events.get(
|
||||
task_or_model_id
|
||||
),
|
||||
last_events=task_last_events.get(task_or_model_id),
|
||||
)
|
||||
now = datetime.utcnow()
|
||||
for model_id in used_model_ids:
|
||||
ModelBLL.update_statistics(
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
model_id=model_id,
|
||||
last_update=now,
|
||||
last_iteration_max=task_iteration.get(model_id),
|
||||
last_scalar_events=task_last_scalar_events.get(model_id),
|
||||
)
|
||||
remaining_tasks = set()
|
||||
for task_id in used_task_ids:
|
||||
# Update related tasks. For reasons of performance, we prefer to update
|
||||
# all of them and not only those who's events were successful
|
||||
updated = self._update_task(
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
task_id=task_id,
|
||||
now=now,
|
||||
iter_max=task_iteration.get(task_id),
|
||||
last_scalar_events=task_last_scalar_events.get(task_id),
|
||||
last_events=task_last_events.get(task_id),
|
||||
)
|
||||
if not updated:
|
||||
remaining_tasks.add(task_id)
|
||||
continue
|
||||
|
||||
if not updated:
|
||||
remaining_tasks.add(task_or_model_id)
|
||||
continue
|
||||
|
||||
if remaining_tasks:
|
||||
TaskBLL.set_last_update(
|
||||
remaining_tasks, company_id, last_update=now
|
||||
)
|
||||
if remaining_tasks:
|
||||
TaskBLL.set_last_update(
|
||||
remaining_tasks,
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
last_update=now,
|
||||
)
|
||||
|
||||
# this is for backwards compatibility with streaming bulk throwing exception on those
|
||||
invalid_iterations_count = errors_per_type.get(invalid_iteration_error)
|
||||
@@ -424,8 +439,22 @@ class EventBLL(object):
|
||||
for k in ("value", "metric", "variant", "iter", "timestamp")
|
||||
if k in event
|
||||
}
|
||||
event_data["min_value"] = min(value, last_event.get("min_value", value))
|
||||
event_data["max_value"] = max(value, last_event.get("max_value", value))
|
||||
last_event_min_value = last_event.get("min_value", value)
|
||||
last_event_min_value_iter = last_event.get("min_value_iter", event_iter)
|
||||
if value < last_event_min_value:
|
||||
event_data["min_value"] = value
|
||||
event_data["min_value_iter"] = event_iter
|
||||
else:
|
||||
event_data["min_value"] = last_event_min_value
|
||||
event_data["min_value_iter"] = last_event_min_value_iter
|
||||
last_event_max_value = last_event.get("max_value", value)
|
||||
last_event_max_value_iter = last_event.get("max_value_iter", event_iter)
|
||||
if value > last_event_max_value:
|
||||
event_data["max_value"] = value
|
||||
event_data["max_value_iter"] = event_iter
|
||||
else:
|
||||
event_data["max_value"] = last_event_max_value
|
||||
event_data["max_value_iter"] = last_event_max_value_iter
|
||||
last_events[metric_hash][variant_hash] = event_data
|
||||
|
||||
def _update_last_metric_events_for_task(self, last_events, event):
|
||||
@@ -445,9 +474,10 @@ class EventBLL(object):
|
||||
|
||||
def _update_task(
|
||||
self,
|
||||
company_id,
|
||||
task_id,
|
||||
now,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
now: datetime,
|
||||
iter_max=None,
|
||||
last_scalar_events=None,
|
||||
last_events=None,
|
||||
@@ -463,8 +493,9 @@ class EventBLL(object):
|
||||
return False
|
||||
|
||||
return TaskBLL.update_statistics(
|
||||
task_id,
|
||||
company_id,
|
||||
task_id=task_id,
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
last_update=now,
|
||||
last_iteration_max=iter_max,
|
||||
last_scalar_events=last_scalar_events,
|
||||
@@ -472,7 +503,9 @@ class EventBLL(object):
|
||||
)
|
||||
|
||||
def _get_event_id(self, event):
|
||||
id_values = (str(event[field]) for field in self.id_fields if field in event)
|
||||
id_values = (
|
||||
str(event[field]) for field in self.event_id_fields if field in event
|
||||
)
|
||||
return hashlib.md5("-".join(id_values).encode()).hexdigest()
|
||||
|
||||
def scroll_task_events(
|
||||
@@ -516,29 +549,45 @@ class EventBLL(object):
|
||||
|
||||
return events, next_scroll_id, total_events
|
||||
|
||||
def get_last_iterations_per_event_metric_variant(
|
||||
def get_task_plots(
|
||||
self,
|
||||
company_id: str,
|
||||
task_id: str,
|
||||
num_last_iterations: int,
|
||||
event_type: EventType,
|
||||
last_iterations_per_plot: int,
|
||||
metric_variants: MetricVariants = None,
|
||||
):
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=event_type):
|
||||
return []
|
||||
event_type = EventType.metrics_plot
|
||||
if check_empty_data(self.es, company_id, event_type):
|
||||
return TaskEventsResult()
|
||||
|
||||
must = [{"term": {"task": task_id}}]
|
||||
plot_valid_condition = {
|
||||
"bool": {
|
||||
"should": [
|
||||
{"term": {PlotFields.valid_plot: True}},
|
||||
{
|
||||
"bool": {
|
||||
"must_not": {"exists": {"field": PlotFields.valid_plot}}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
must = [plot_valid_condition, {"term": {"task": task_id}}]
|
||||
if metric_variants:
|
||||
must.append(get_metric_variants_condition(metric_variants))
|
||||
|
||||
query = {"bool": {"must": must}}
|
||||
search_args = dict(es=self.es, company_id=company_id, event_type=event_type)
|
||||
max_metrics, max_variants = get_max_metric_and_variant_counts(
|
||||
query=query, **search_args,
|
||||
query=query,
|
||||
**search_args,
|
||||
)
|
||||
max_variants = int(max_variants // num_last_iterations)
|
||||
max_variants = int(max_variants // last_iterations_per_plot)
|
||||
|
||||
es_req: dict = {
|
||||
es_req = {
|
||||
"sort": [{"iter": {"order": "desc"}}],
|
||||
"size": 0,
|
||||
"query": query,
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
@@ -554,11 +603,10 @@ class EventBLL(object):
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"iters": {
|
||||
"terms": {
|
||||
"field": "iter",
|
||||
"size": num_last_iterations,
|
||||
"order": {"_key": "desc"},
|
||||
"events": {
|
||||
"top_hits": {
|
||||
"sort": {"iter": {"order": "desc"}},
|
||||
"size": last_iterations_per_plot,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -566,117 +614,23 @@ class EventBLL(object):
|
||||
},
|
||||
}
|
||||
},
|
||||
"query": query,
|
||||
}
|
||||
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(body=es_req, **search_args)
|
||||
es_response = search_company_events(body=es_req, ignore=404, **search_args)
|
||||
|
||||
if "aggregations" not in es_res:
|
||||
return []
|
||||
|
||||
return [
|
||||
(metric["key"], variant["key"], iter["key"])
|
||||
for metric in es_res["aggregations"]["metrics"]["buckets"]
|
||||
for variant in metric["variants"]["buckets"]
|
||||
for iter in variant["iters"]["buckets"]
|
||||
]
|
||||
|
||||
def get_task_plots(
|
||||
self,
|
||||
company_id: str,
|
||||
tasks: Sequence[str],
|
||||
last_iterations_per_plot: int = None,
|
||||
sort=None,
|
||||
size: int = 500,
|
||||
scroll_id: str = None,
|
||||
no_scroll: bool = False,
|
||||
metric_variants: MetricVariants = None,
|
||||
model_events: bool = False,
|
||||
):
|
||||
if scroll_id == self.empty_scroll:
|
||||
aggs_result = es_response.get("aggregations")
|
||||
if not aggs_result:
|
||||
return TaskEventsResult()
|
||||
|
||||
if scroll_id:
|
||||
with translate_errors_context():
|
||||
es_res = self.es.scroll(scroll_id=scroll_id, scroll="1h")
|
||||
else:
|
||||
event_type = EventType.metrics_plot
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=event_type):
|
||||
return TaskEventsResult()
|
||||
|
||||
plot_valid_condition = {
|
||||
"bool": {
|
||||
"should": [
|
||||
{"term": {PlotFields.valid_plot: True}},
|
||||
{
|
||||
"bool": {
|
||||
"must_not": {"exists": {"field": PlotFields.valid_plot}}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
must = [plot_valid_condition]
|
||||
|
||||
if last_iterations_per_plot is None or model_events:
|
||||
must.append({"terms": {"task": tasks}})
|
||||
if metric_variants:
|
||||
must.append(get_metric_variants_condition(metric_variants))
|
||||
else:
|
||||
should = []
|
||||
for i, task_id in enumerate(tasks):
|
||||
last_iters = self.get_last_iterations_per_event_metric_variant(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
num_last_iterations=last_iterations_per_plot,
|
||||
event_type=event_type,
|
||||
metric_variants=metric_variants,
|
||||
)
|
||||
if not last_iters:
|
||||
continue
|
||||
|
||||
for metric, variant, iter in last_iters:
|
||||
should.append(
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"task": task_id}},
|
||||
{"term": {"metric": metric}},
|
||||
{"term": {"variant": variant}},
|
||||
{"term": {"iter": iter}},
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
if not should:
|
||||
return TaskEventsResult()
|
||||
must.append({"bool": {"should": should}})
|
||||
|
||||
if sort is None:
|
||||
sort = [{"timestamp": {"order": "asc"}}]
|
||||
|
||||
es_req = {
|
||||
"sort": sort,
|
||||
"size": min(size, 10000),
|
||||
"query": {"bool": {"must": must}},
|
||||
}
|
||||
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
ignore=404,
|
||||
**({} if no_scroll else {"scroll": "1h"}),
|
||||
)
|
||||
|
||||
events, total_events, next_scroll_id = self._get_events_from_es_res(es_res)
|
||||
events = [
|
||||
hit["_source"]
|
||||
for metrics_bucket in aggs_result["metrics"]["buckets"]
|
||||
for variants_bucket in metrics_bucket["variants"]["buckets"]
|
||||
for hit in variants_bucket["events"]["hits"]["hits"]
|
||||
]
|
||||
self.uncompress_plots(events)
|
||||
return TaskEventsResult(
|
||||
events=events, next_scroll_id=next_scroll_id, total_events=total_events
|
||||
)
|
||||
return TaskEventsResult(events=events, total_events=len(events))
|
||||
|
||||
def _get_events_from_es_res(self, es_res: dict) -> Tuple[list, int, Optional[str]]:
|
||||
"""
|
||||
@@ -693,9 +647,11 @@ class EventBLL(object):
|
||||
return events, total_events, next_scroll_id
|
||||
|
||||
def get_debug_image_urls(
|
||||
self, company_id: str, task_id: str, after_key: dict = None
|
||||
self, company_id: str, task_ids: Sequence[str], after_key: dict = None
|
||||
) -> Tuple[Sequence[str], Optional[dict]]:
|
||||
if check_empty_data(self.es, company_id, EventType.metrics_image):
|
||||
if not task_ids or check_empty_data(
|
||||
self.es, company_id, EventType.metrics_image
|
||||
):
|
||||
return [], None
|
||||
|
||||
es_req = {
|
||||
@@ -711,7 +667,10 @@ class EventBLL(object):
|
||||
},
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [{"term": {"task": task_id}}, {"exists": {"field": "url"}}]
|
||||
"must": [
|
||||
{"terms": {"task": task_ids}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -729,9 +688,13 @@ class EventBLL(object):
|
||||
return [bucket["key"]["url"] for bucket in res["buckets"]], res.get("after_key")
|
||||
|
||||
def get_plot_image_urls(
|
||||
self, company_id: str, task_id: str, scroll_id: Optional[str]
|
||||
self, company_id: str, task_ids: Sequence[str], scroll_id: Optional[str]
|
||||
) -> Tuple[Sequence[dict], Optional[str]]:
|
||||
if scroll_id == self.empty_scroll:
|
||||
if (
|
||||
scroll_id == self.empty_scroll
|
||||
or not task_ids
|
||||
or check_empty_data(self.es, company_id, EventType.metrics_plot)
|
||||
):
|
||||
return [], None
|
||||
|
||||
if scroll_id:
|
||||
@@ -746,7 +709,7 @@ class EventBLL(object):
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"task": task_id}},
|
||||
{"terms": {"task": task_ids}},
|
||||
{"exists": {"field": PlotFields.source_urls}},
|
||||
]
|
||||
}
|
||||
@@ -765,17 +728,16 @@ class EventBLL(object):
|
||||
|
||||
def get_task_events(
|
||||
self,
|
||||
company_id: str,
|
||||
task_id: str,
|
||||
company_id: Union[str, Sequence[str]],
|
||||
task_id: Union[str, Sequence[str]],
|
||||
event_type: EventType,
|
||||
metric=None,
|
||||
variant=None,
|
||||
metrics: MetricVariants = None,
|
||||
last_iter_count=None,
|
||||
sort=None,
|
||||
size=500,
|
||||
scroll_id=None,
|
||||
no_scroll=False,
|
||||
model_events=False,
|
||||
last_iters_per_task_metric=False,
|
||||
) -> TaskEventsResult:
|
||||
if scroll_id == self.empty_scroll:
|
||||
return TaskEventsResult()
|
||||
@@ -784,38 +746,65 @@ class EventBLL(object):
|
||||
with translate_errors_context():
|
||||
es_res = self.es.scroll(scroll_id=scroll_id, scroll="1h")
|
||||
else:
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=event_type):
|
||||
company_ids = [company_id] if isinstance(company_id, str) else company_id
|
||||
company_ids = [
|
||||
c_id
|
||||
for c_id in set(company_ids)
|
||||
if not check_empty_data(self.es, c_id, event_type)
|
||||
]
|
||||
if not company_ids:
|
||||
return TaskEventsResult()
|
||||
|
||||
task_ids = [task_id] if isinstance(task_id, str) else task_id
|
||||
|
||||
must = []
|
||||
if metric:
|
||||
must.append({"term": {"metric": metric}})
|
||||
if variant:
|
||||
must.append({"term": {"variant": variant}})
|
||||
if metrics:
|
||||
must.append(get_metric_variants_condition(metrics))
|
||||
|
||||
if last_iter_count is None or model_events:
|
||||
if last_iter_count is None:
|
||||
must.append({"terms": {"task": task_ids}})
|
||||
else:
|
||||
tasks_iters = self.get_last_iters(
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
task_id=task_ids,
|
||||
iters=last_iter_count,
|
||||
)
|
||||
should = [
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"task": task}},
|
||||
{"terms": {"iter": last_iters}},
|
||||
]
|
||||
if last_iters_per_task_metric:
|
||||
task_metric_iters = self.get_last_iters_per_metric(
|
||||
company_id=company_ids,
|
||||
event_type=event_type,
|
||||
task_id=task_ids,
|
||||
iters=last_iter_count,
|
||||
metrics=metrics,
|
||||
)
|
||||
should = [
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"task": task}},
|
||||
{"term": {"metric": metric}},
|
||||
{"terms": {"iter": last_iters}},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
for task, last_iters in tasks_iters.items()
|
||||
if last_iters
|
||||
]
|
||||
for (task, metric), last_iters in task_metric_iters.items()
|
||||
if last_iters
|
||||
]
|
||||
else:
|
||||
tasks_iters = self.get_last_iters(
|
||||
company_id=company_ids,
|
||||
event_type=event_type,
|
||||
task_id=task_ids,
|
||||
iters=last_iter_count,
|
||||
metrics=metrics,
|
||||
)
|
||||
should = [
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"task": task}},
|
||||
{"terms": {"iter": last_iters}},
|
||||
]
|
||||
}
|
||||
}
|
||||
for task, last_iters in tasks_iters.items()
|
||||
if last_iters
|
||||
]
|
||||
if not should:
|
||||
return TaskEventsResult()
|
||||
must.append({"bool": {"should": should}})
|
||||
@@ -832,7 +821,7 @@ class EventBLL(object):
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
company_id=company_ids,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
ignore=404,
|
||||
@@ -856,7 +845,8 @@ class EventBLL(object):
|
||||
query = {"bool": {"must": [{"term": {"task": task_id}}]}}
|
||||
search_args = dict(es=self.es, company_id=company_id, event_type=event_type)
|
||||
max_metrics, max_variants = get_max_metric_and_variant_counts(
|
||||
query=query, **search_args,
|
||||
query=query,
|
||||
**search_args,
|
||||
)
|
||||
es_req = {
|
||||
"size": 0,
|
||||
@@ -910,8 +900,10 @@ class EventBLL(object):
|
||||
}
|
||||
search_args = dict(es=self.es, company_id=company_id, event_type=event_type)
|
||||
max_metrics, max_variants = get_max_metric_and_variant_counts(
|
||||
query=query, **search_args,
|
||||
query=query,
|
||||
**search_args,
|
||||
)
|
||||
max_variants = int(max_variants // 2)
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
@@ -1013,22 +1005,98 @@ class EventBLL(object):
|
||||
|
||||
return iterations, vectors
|
||||
|
||||
def get_last_iters(
|
||||
def get_last_iters_per_metric(
|
||||
self,
|
||||
company_id: str,
|
||||
company_id: Union[str, Sequence[str]],
|
||||
event_type: EventType,
|
||||
task_id: Union[str, Sequence[str]],
|
||||
iters: int,
|
||||
) -> Mapping[str, Sequence]:
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=event_type):
|
||||
metrics: MetricVariants = None,
|
||||
) -> Mapping[Tuple[str, str], Sequence]:
|
||||
company_ids = [company_id] if isinstance(company_id, str) else company_id
|
||||
company_ids = [
|
||||
c_id
|
||||
for c_id in set(company_ids)
|
||||
if not check_empty_data(self.es, c_id, event_type)
|
||||
]
|
||||
if not company_ids:
|
||||
return {}
|
||||
|
||||
task_ids = [task_id] if isinstance(task_id, str) else task_id
|
||||
must = [{"terms": {"task": task_ids}}]
|
||||
if metrics:
|
||||
must.append(get_metric_variants_condition(metrics))
|
||||
|
||||
max_tasks = min(len(task_ids), 1000)
|
||||
max_metrics = 10_000 // (max_tasks * iters)
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"aggs": {
|
||||
"tasks": {
|
||||
"terms": {"field": "task"},
|
||||
"terms": {"field": "task", "size": max_tasks},
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {"field": "metric", "size": max_metrics},
|
||||
"aggs": {
|
||||
"iters": {
|
||||
"terms": {
|
||||
"field": "iter",
|
||||
"size": iters,
|
||||
"order": {"_key": "desc"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"query": {"bool": {"must": must}},
|
||||
}
|
||||
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_ids,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
)
|
||||
if "aggregations" not in es_res:
|
||||
return {}
|
||||
|
||||
return {
|
||||
(tb["key"], mb["key"]): [ib["key"] for ib in mb["iters"]["buckets"]]
|
||||
for tb in es_res["aggregations"]["tasks"]["buckets"]
|
||||
for mb in tb["metrics"]["buckets"]
|
||||
}
|
||||
|
||||
def get_last_iters(
|
||||
self,
|
||||
company_id: Union[str, Sequence[str]],
|
||||
event_type: EventType,
|
||||
task_id: Union[str, Sequence[str]],
|
||||
iters: int,
|
||||
metrics: MetricVariants = None,
|
||||
) -> Mapping[str, Sequence]:
|
||||
company_ids = [company_id] if isinstance(company_id, str) else company_id
|
||||
company_ids = [
|
||||
c_id
|
||||
for c_id in set(company_ids)
|
||||
if not check_empty_data(self.es, c_id, event_type)
|
||||
]
|
||||
if not company_ids:
|
||||
return {}
|
||||
|
||||
task_ids = [task_id] if isinstance(task_id, str) else task_id
|
||||
must = [{"terms": {"task": task_ids}}]
|
||||
if metrics:
|
||||
must.append(get_metric_variants_condition(metrics))
|
||||
|
||||
max_tasks = min(len(task_ids), 1000)
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"aggs": {
|
||||
"tasks": {
|
||||
"terms": {"field": "task", "size": max_tasks},
|
||||
"aggs": {
|
||||
"iters": {
|
||||
"terms": {
|
||||
@@ -1040,12 +1108,15 @@ class EventBLL(object):
|
||||
},
|
||||
}
|
||||
},
|
||||
"query": {"bool": {"must": [{"terms": {"task": task_ids}}]}},
|
||||
"query": {"bool": {"must": must}},
|
||||
}
|
||||
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req,
|
||||
self.es,
|
||||
company_id=company_ids,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
)
|
||||
|
||||
if "aggregations" not in es_res:
|
||||
@@ -1096,18 +1167,26 @@ class EventBLL(object):
|
||||
|
||||
return {"refresh": True}
|
||||
|
||||
def delete_task_events(
|
||||
self, company_id, task_id, allow_locked=False, model=False, async_delete=False,
|
||||
):
|
||||
def delete_task_events(self, company_id, task_id, allow_locked=False, model=False):
|
||||
if model:
|
||||
self._validate_model_state(
|
||||
company_id=company_id, model_id=task_id, allow_locked=allow_locked,
|
||||
company_id=company_id,
|
||||
model_id=task_id,
|
||||
allow_locked=allow_locked,
|
||||
)
|
||||
else:
|
||||
self._validate_task_state(
|
||||
company_id=company_id, task_id=task_id, allow_locked=allow_locked
|
||||
)
|
||||
|
||||
async_delete = async_task_events_delete
|
||||
if async_delete:
|
||||
total = self.events_iterator.count_task_events(
|
||||
event_type=EventType.all,
|
||||
company_id=company_id,
|
||||
task_ids=[task_id],
|
||||
)
|
||||
if total <= async_delete_threshold:
|
||||
async_delete = False
|
||||
es_req = {"query": {"term": {"task": task_id}}}
|
||||
with translate_errors_context():
|
||||
es_res = delete_company_events(
|
||||
@@ -1165,24 +1244,37 @@ class EventBLL(object):
|
||||
return es_res.get("deleted", 0)
|
||||
|
||||
def delete_multi_task_events(
|
||||
self, company_id: str, task_ids: Sequence[str], async_delete=False
|
||||
self, company_id: str, task_ids: Sequence[str], model=False
|
||||
):
|
||||
"""
|
||||
Delete mutliple task events. No check is done for tasks write access
|
||||
Delete multiple 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}}}
|
||||
deleted = 0
|
||||
with translate_errors_context():
|
||||
es_res = delete_company_events(
|
||||
es=self.es,
|
||||
company_id=company_id,
|
||||
event_type=EventType.all,
|
||||
body=es_req,
|
||||
**self._get_events_deletion_params(async_delete),
|
||||
)
|
||||
async_delete = async_task_events_delete
|
||||
if async_delete and len(task_ids) < 100:
|
||||
total = self.events_iterator.count_task_events(
|
||||
event_type=EventType.all,
|
||||
company_id=company_id,
|
||||
task_ids=task_ids,
|
||||
)
|
||||
if total <= async_delete_threshold:
|
||||
async_delete = False
|
||||
for tasks in chunked_iter(task_ids, 100):
|
||||
es_req = {"query": {"terms": {"task": tasks}}}
|
||||
es_res = delete_company_events(
|
||||
es=self.es,
|
||||
company_id=company_id,
|
||||
event_type=EventType.all,
|
||||
body=es_req,
|
||||
**self._get_events_deletion_params(async_delete),
|
||||
)
|
||||
if not async_delete:
|
||||
deleted += es_res.get("deleted", 0)
|
||||
|
||||
if not async_delete:
|
||||
return es_res.get("deleted", 0)
|
||||
return deleted
|
||||
|
||||
def clear_scroll(self, scroll_id: str):
|
||||
if scroll_id == self.empty_scroll:
|
||||
|
||||
@@ -8,6 +8,7 @@ from elasticsearch import Elasticsearch
|
||||
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.tools import safe_get
|
||||
|
||||
|
||||
@@ -22,6 +23,7 @@ class EventType(Enum):
|
||||
|
||||
SINGLE_SCALAR_ITERATION = -(2 ** 31)
|
||||
MetricVariants = Mapping[str, Sequence[str]]
|
||||
TaskCompanies = Mapping[str, Sequence[Task]]
|
||||
|
||||
|
||||
class EventSettings:
|
||||
@@ -52,14 +54,17 @@ class EventSettings:
|
||||
return int(self._max_es_allowed_aggregation_buckets * percentage)
|
||||
|
||||
|
||||
def get_index_name(company_id: str, event_type: str):
|
||||
def get_index_name(company_id: Union[str, Sequence[str]], event_type: str):
|
||||
event_type = event_type.lower().replace(" ", "_")
|
||||
return f"events-{event_type}-{company_id.lower()}"
|
||||
if isinstance(company_id, str):
|
||||
company_id = [company_id]
|
||||
|
||||
return ",".join(f"events-{event_type}-{(c_id or '').lower()}" for c_id in company_id)
|
||||
|
||||
|
||||
def check_empty_data(es: Elasticsearch, company_id: str, event_type: EventType) -> bool:
|
||||
es_index = get_index_name(company_id, event_type.value)
|
||||
if not es.indices.exists(es_index):
|
||||
if not es.indices.exists(index=es_index):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ from apiserver.bll.event.event_common import (
|
||||
get_metric_variants_condition,
|
||||
get_max_metric_and_variant_counts,
|
||||
SINGLE_SCALAR_ITERATION,
|
||||
TaskCompanies,
|
||||
)
|
||||
from apiserver.bll.event.scalar_key import ScalarKey, ScalarKeyEnum
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.tools import safe_get
|
||||
|
||||
log = config.logger(__file__)
|
||||
@@ -108,37 +108,51 @@ class EventMetrics:
|
||||
|
||||
def compare_scalar_metrics_average_per_iter(
|
||||
self,
|
||||
company_id,
|
||||
tasks: Sequence[Task],
|
||||
companies: TaskCompanies,
|
||||
samples,
|
||||
key: ScalarKeyEnum,
|
||||
metric_variants: MetricVariants = None,
|
||||
):
|
||||
"""
|
||||
Compare scalar metrics for different tasks per metric and variant
|
||||
The amount of points in each histogram should not exceed the requested samples
|
||||
"""
|
||||
event_type = EventType.metrics_scalar
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=event_type):
|
||||
companies = {
|
||||
company_id: tasks
|
||||
for company_id, tasks in companies.items()
|
||||
if not check_empty_data(
|
||||
self.es, company_id=company_id, event_type=event_type
|
||||
)
|
||||
}
|
||||
if not companies:
|
||||
return {}
|
||||
|
||||
task_name_by_id = {t.id: t.name for t in tasks}
|
||||
get_scalar_average_per_iter = partial(
|
||||
self._get_scalar_average_per_iter_core,
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
samples=samples,
|
||||
key=ScalarKey.resolve(key),
|
||||
metric_variants=metric_variants,
|
||||
run_parallel=False,
|
||||
)
|
||||
task_ids = [t.id for t in tasks]
|
||||
task_ids, company_ids = zip(
|
||||
*(
|
||||
(t.id, t.company)
|
||||
for t in itertools.chain.from_iterable(companies.values())
|
||||
)
|
||||
)
|
||||
with ThreadPoolExecutor(max_workers=EventSettings.max_workers) as pool:
|
||||
task_metrics = zip(
|
||||
task_ids, pool.map(get_scalar_average_per_iter, task_ids)
|
||||
task_ids, pool.map(get_scalar_average_per_iter, task_ids, company_ids)
|
||||
)
|
||||
|
||||
task_names = {
|
||||
t.id: t.name for t in itertools.chain.from_iterable(companies.values())
|
||||
}
|
||||
res = defaultdict(lambda: defaultdict(dict))
|
||||
for task_id, task_data in task_metrics:
|
||||
task_name = task_name_by_id[task_id]
|
||||
task_name = task_names[task_id]
|
||||
for metric_key, metric_data in task_data.items():
|
||||
for variant_key, variant_data in metric_data.items():
|
||||
variant_data["name"] = task_name
|
||||
@@ -147,18 +161,27 @@ class EventMetrics:
|
||||
return res
|
||||
|
||||
def get_task_single_value_metrics(
|
||||
self, company_id: str, tasks: Sequence[Task]
|
||||
self, companies: TaskCompanies
|
||||
) -> Mapping[str, dict]:
|
||||
"""
|
||||
For the requested tasks return all the events delivered for the single iteration (-2**31)
|
||||
"""
|
||||
if check_empty_data(
|
||||
self.es, company_id=company_id, event_type=EventType.metrics_scalar
|
||||
):
|
||||
companies = {
|
||||
company_id: [t.id for t in tasks]
|
||||
for company_id, tasks in companies.items()
|
||||
if not check_empty_data(
|
||||
self.es, company_id=company_id, event_type=EventType.metrics_scalar
|
||||
)
|
||||
}
|
||||
if not companies:
|
||||
return {}
|
||||
|
||||
task_ids = [t.id for t in tasks]
|
||||
task_events = self._get_task_single_value_metrics(company_id, task_ids)
|
||||
with ThreadPoolExecutor(max_workers=EventSettings.max_workers) as pool:
|
||||
task_events = list(
|
||||
itertools.chain.from_iterable(
|
||||
pool.map(self._get_task_single_value_metrics, companies.items())
|
||||
),
|
||||
)
|
||||
|
||||
def _get_value(event: dict):
|
||||
return {
|
||||
@@ -172,8 +195,9 @@ class EventMetrics:
|
||||
}
|
||||
|
||||
def _get_task_single_value_metrics(
|
||||
self, company_id: str, task_ids: Sequence[str]
|
||||
self, tasks: Tuple[str, Sequence[str]]
|
||||
) -> Sequence[dict]:
|
||||
company_id, task_ids = tasks
|
||||
es_req = {
|
||||
"size": 10000,
|
||||
"query": {
|
||||
|
||||
@@ -64,13 +64,13 @@ class EventsIterator:
|
||||
self,
|
||||
event_type: EventType,
|
||||
company_id: str,
|
||||
task_id: str,
|
||||
task_ids: Sequence[str],
|
||||
metric_variants: MetricVariants = None,
|
||||
) -> int:
|
||||
if check_empty_data(self.es, company_id, event_type):
|
||||
return 0
|
||||
|
||||
query, _ = self._get_initial_query_and_must(task_id, metric_variants)
|
||||
query, _ = self._get_initial_query_and_must(task_ids, metric_variants)
|
||||
es_req = {
|
||||
"query": query,
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class EventsIterator:
|
||||
For the last key-field value all the events are brought (even if the resulting size exceeds batch_size)
|
||||
so that events with this value will not be lost between the calls.
|
||||
"""
|
||||
query, must = self._get_initial_query_and_must(task_id, metric_variants)
|
||||
query, must = self._get_initial_query_and_must([task_id], metric_variants)
|
||||
|
||||
# retrieve the next batch of events
|
||||
es_req = {
|
||||
@@ -158,14 +158,14 @@ class EventsIterator:
|
||||
|
||||
@staticmethod
|
||||
def _get_initial_query_and_must(
|
||||
task_id: str, metric_variants: MetricVariants = None
|
||||
task_ids: Sequence[str], metric_variants: MetricVariants = None
|
||||
) -> Tuple[dict, list]:
|
||||
if not metric_variants:
|
||||
must = [{"term": {"task": task_id}}]
|
||||
query = {"term": {"task": task_id}}
|
||||
query = {"terms": {"task": task_ids}}
|
||||
must = [query]
|
||||
else:
|
||||
must = [
|
||||
{"term": {"task": task_id}},
|
||||
{"terms": {"task": task_ids}},
|
||||
get_metric_variants_condition(metric_variants),
|
||||
]
|
||||
query = {"bool": {"must": must}}
|
||||
|
||||
@@ -75,18 +75,25 @@ class MetricEventsIterator:
|
||||
|
||||
def get_task_events(
|
||||
self,
|
||||
company_id: str,
|
||||
companies: Mapping[str, str],
|
||||
task_metrics: Mapping[str, dict],
|
||||
iter_count: int,
|
||||
navigate_earlier: bool = True,
|
||||
refresh: bool = False,
|
||||
state_id: str = None,
|
||||
) -> MetricEventsResult:
|
||||
if check_empty_data(self.es, company_id, self.event_type):
|
||||
companies = {
|
||||
task_id: company_id
|
||||
for task_id, company_id in companies.items()
|
||||
if not check_empty_data(
|
||||
self.es, company_id=company_id, event_type=self.event_type
|
||||
)
|
||||
}
|
||||
if not companies:
|
||||
return MetricEventsResult()
|
||||
|
||||
def init_state(state_: MetricEventsScrollState):
|
||||
state_.tasks = self._init_task_states(company_id, task_metrics)
|
||||
state_.tasks = self._init_task_states(companies, task_metrics)
|
||||
|
||||
def validate_state(state_: MetricEventsScrollState):
|
||||
"""
|
||||
@@ -95,7 +102,7 @@ class MetricEventsIterator:
|
||||
Refresh the state if requested
|
||||
"""
|
||||
if refresh:
|
||||
self._reinit_outdated_task_states(company_id, state_, task_metrics)
|
||||
self._reinit_outdated_task_states(companies, state_, task_metrics)
|
||||
|
||||
with self.cache_manager.get_or_create_state(
|
||||
state_id=state_id, init_state=init_state, validate_state=validate_state
|
||||
@@ -112,7 +119,7 @@ class MetricEventsIterator:
|
||||
pool.map(
|
||||
partial(
|
||||
self._get_task_metric_events,
|
||||
company_id=company_id,
|
||||
companies=companies,
|
||||
iter_count=iter_count,
|
||||
navigate_earlier=navigate_earlier,
|
||||
specific_variants_requested=specific_variants_requested,
|
||||
@@ -125,7 +132,7 @@ class MetricEventsIterator:
|
||||
|
||||
def _reinit_outdated_task_states(
|
||||
self,
|
||||
company_id,
|
||||
companies: Mapping[str, str],
|
||||
state: MetricEventsScrollState,
|
||||
task_metrics: Mapping[str, dict],
|
||||
):
|
||||
@@ -133,9 +140,7 @@ class MetricEventsIterator:
|
||||
Determine the metrics for which new event_type events were added
|
||||
since their states were initialized and re-init these states
|
||||
"""
|
||||
tasks = Task.objects(id__in=list(task_metrics), company=company_id).only(
|
||||
"id", "metric_stats"
|
||||
)
|
||||
tasks = Task.objects(id__in=list(task_metrics)).only("id", "metric_stats")
|
||||
|
||||
def get_last_update_times_for_task_metrics(
|
||||
task: Task,
|
||||
@@ -175,7 +180,7 @@ class MetricEventsIterator:
|
||||
if metrics_to_recalc:
|
||||
task_metrics_to_recalc[task] = metrics_to_recalc
|
||||
|
||||
updated_task_states = self._init_task_states(company_id, task_metrics_to_recalc)
|
||||
updated_task_states = self._init_task_states(companies, task_metrics_to_recalc)
|
||||
|
||||
def merge_with_updated_task_states(
|
||||
old_state: TaskScrollState, updates: Sequence[TaskScrollState]
|
||||
@@ -205,14 +210,14 @@ class MetricEventsIterator:
|
||||
]
|
||||
|
||||
def _init_task_states(
|
||||
self, company_id: str, task_metrics: Mapping[str, dict]
|
||||
self, companies: Mapping[str, str], task_metrics: Mapping[str, dict]
|
||||
) -> Sequence[TaskScrollState]:
|
||||
"""
|
||||
Returned initialized metric scroll stated for the requested task metrics
|
||||
"""
|
||||
with ThreadPoolExecutor(EventSettings.max_workers) as pool:
|
||||
task_metric_states = pool.map(
|
||||
partial(self._init_metric_states_for_task, company_id=company_id),
|
||||
partial(self._init_metric_states_for_task, companies=companies),
|
||||
task_metrics.items(),
|
||||
)
|
||||
|
||||
@@ -232,13 +237,14 @@ class MetricEventsIterator:
|
||||
pass
|
||||
|
||||
def _init_metric_states_for_task(
|
||||
self, task_metrics: Tuple[str, dict], company_id: str
|
||||
self, task_metrics: Tuple[str, dict], companies: Mapping[str, str]
|
||||
) -> Sequence[MetricState]:
|
||||
"""
|
||||
Return metric scroll states for the task filled with the variant states
|
||||
for the variants that reported any event_type events
|
||||
"""
|
||||
task, metrics = task_metrics
|
||||
company_id = companies[task]
|
||||
must = [{"term": {"task": task}}, *self._get_extra_conditions()]
|
||||
if metrics:
|
||||
must.append(get_metric_variants_condition(metrics))
|
||||
@@ -319,7 +325,7 @@ class MetricEventsIterator:
|
||||
def _get_task_metric_events(
|
||||
self,
|
||||
task_state: TaskScrollState,
|
||||
company_id: str,
|
||||
companies: Mapping[str, str],
|
||||
iter_count: int,
|
||||
navigate_earlier: bool,
|
||||
specific_variants_requested: bool,
|
||||
@@ -391,7 +397,10 @@ class MetricEventsIterator:
|
||||
}
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=self.event_type, body=es_req,
|
||||
self.es,
|
||||
company_id=companies[task_state.task],
|
||||
event_type=self.event_type,
|
||||
body=es_req,
|
||||
)
|
||||
if "aggregations" not in es_res:
|
||||
return task_state.task, []
|
||||
|
||||
@@ -5,7 +5,8 @@ from mongoengine import Q
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels.models import ModelTaskPublishResponse
|
||||
from apiserver.bll.task.utils import deleted_prefix
|
||||
from apiserver.bll.task.utils import deleted_prefix, get_last_metric_updates
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.task.task import Task, TaskStatus
|
||||
@@ -13,6 +14,8 @@ from .metadata import Metadata
|
||||
|
||||
|
||||
class ModelBLL:
|
||||
event_bll = None
|
||||
|
||||
@classmethod
|
||||
def get_company_model_by_id(
|
||||
cls, company_id: str, model_id: str, only_fields=None
|
||||
@@ -28,11 +31,7 @@ class ModelBLL:
|
||||
|
||||
@staticmethod
|
||||
def assert_exists(
|
||||
company_id,
|
||||
model_ids,
|
||||
only=None,
|
||||
allow_public=False,
|
||||
return_models=True,
|
||||
company_id, model_ids, only=None, allow_public=False, return_models=True,
|
||||
) -> Optional[Sequence[Model]]:
|
||||
model_ids = [model_ids] if isinstance(model_ids, str) else model_ids
|
||||
ids = set(model_ids)
|
||||
@@ -58,8 +57,9 @@ class ModelBLL:
|
||||
cls,
|
||||
model_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
force_publish_task: bool = False,
|
||||
publish_task_func: Callable[[str, str, bool], dict] = None,
|
||||
publish_task_func: Callable[[str, str, str, bool], dict] = None,
|
||||
) -> Tuple[int, ModelTaskPublishResponse]:
|
||||
model = cls.get_company_model_by_id(company_id=company_id, model_id=model_id)
|
||||
if model.ready:
|
||||
@@ -74,18 +74,25 @@ class ModelBLL:
|
||||
)
|
||||
if task and task.status != TaskStatus.published:
|
||||
task_publish_res = publish_task_func(
|
||||
model.task, company_id, force_publish_task
|
||||
model.task, company_id, user_id, force_publish_task
|
||||
)
|
||||
published_task = ModelTaskPublishResponse(
|
||||
id=model.task, data=task_publish_res
|
||||
)
|
||||
|
||||
updated = model.update(upsert=False, ready=True, last_update=datetime.utcnow())
|
||||
now = datetime.utcnow()
|
||||
updated = model.update(
|
||||
upsert=False,
|
||||
ready=True,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=user_id,
|
||||
)
|
||||
return updated, published_task
|
||||
|
||||
@classmethod
|
||||
def delete_model(
|
||||
cls, model_id: str, company_id: str, force: bool
|
||||
cls, model_id: str, company_id: str, user_id: str, force: bool, delete_external_artifacts: bool = True,
|
||||
) -> Tuple[int, Model]:
|
||||
model = cls.get_company_model_by_id(
|
||||
company_id=company_id,
|
||||
@@ -111,49 +118,88 @@ class ModelBLL:
|
||||
|
||||
if model.task:
|
||||
task = Task.objects(id=model.task).first()
|
||||
if task and task.status == TaskStatus.published:
|
||||
if not force:
|
||||
raise errors.bad_request.ModelCreatingTaskExists(
|
||||
"and published, use force=True to delete", task=model.task
|
||||
)
|
||||
if task.models.output and model_id in task.models.output:
|
||||
now = datetime.utcnow()
|
||||
if task:
|
||||
now = datetime.utcnow()
|
||||
if task.status == TaskStatus.published:
|
||||
if not force:
|
||||
raise errors.bad_request.ModelCreatingTaskExists(
|
||||
"and published, use force=True to delete", task=model.task
|
||||
)
|
||||
Task._get_collection().update_one(
|
||||
filter={"_id": model.task, "models.output.model": model_id},
|
||||
update={
|
||||
"$set": {
|
||||
"models.output.$[elem].model": deleted_model_id,
|
||||
"output.error": f"model deleted on {now.isoformat()}",
|
||||
"last_change": now,
|
||||
"last_changed_by": user_id,
|
||||
},
|
||||
"last_change": now,
|
||||
},
|
||||
array_filters=[{"elem.model": model_id}],
|
||||
upsert=False,
|
||||
)
|
||||
else:
|
||||
task.update(
|
||||
pull__models__output__model=model_id,
|
||||
set__last_change=now,
|
||||
set__last_changed_by=user_id,
|
||||
)
|
||||
|
||||
delete_external_artifacts = delete_external_artifacts and config.get(
|
||||
"services.async_urls_delete.enabled", True
|
||||
)
|
||||
if delete_external_artifacts:
|
||||
from apiserver.bll.task.task_cleanup import (
|
||||
collect_debug_image_urls,
|
||||
collect_plot_image_urls,
|
||||
_schedule_for_delete,
|
||||
)
|
||||
urls = set()
|
||||
urls.update(collect_debug_image_urls(company_id, model_id))
|
||||
urls.update(collect_plot_image_urls(company_id, model_id))
|
||||
if model.uri:
|
||||
urls.add(model.uri)
|
||||
if urls:
|
||||
_schedule_for_delete(
|
||||
task_id=model_id,
|
||||
company=company_id,
|
||||
user=user_id,
|
||||
urls=urls,
|
||||
can_delete_folders=False,
|
||||
)
|
||||
|
||||
if not cls.event_bll:
|
||||
from apiserver.bll.event import EventBLL
|
||||
cls.event_bll = EventBLL()
|
||||
|
||||
cls.event_bll.delete_task_events(company_id, model_id, allow_locked=True, model=True)
|
||||
del_count = Model.objects(id=model_id, company=company_id).delete()
|
||||
return del_count, model
|
||||
|
||||
@classmethod
|
||||
def archive_model(cls, model_id: str, company_id: str):
|
||||
def archive_model(cls, model_id: str, company_id: str, user_id: str):
|
||||
cls.get_company_model_by_id(
|
||||
company_id=company_id, model_id=model_id, only_fields=("id",)
|
||||
)
|
||||
now = datetime.utcnow()
|
||||
archived = Model.objects(company=company_id, id=model_id).update(
|
||||
add_to_set__system_tags=EntityVisibility.archived.value,
|
||||
last_update=datetime.utcnow(),
|
||||
last_change=now,
|
||||
last_changed_by=user_id,
|
||||
)
|
||||
|
||||
return archived
|
||||
|
||||
@classmethod
|
||||
def unarchive_model(cls, model_id: str, company_id: str):
|
||||
def unarchive_model(cls, model_id: str, company_id: str, user_id: str):
|
||||
cls.get_company_model_by_id(
|
||||
company_id=company_id, model_id=model_id, only_fields=("id",)
|
||||
)
|
||||
now = datetime.utcnow()
|
||||
unarchived = Model.objects(company=company_id, id=model_id).update(
|
||||
pull__system_tags=EntityVisibility.archived.value,
|
||||
last_update=datetime.utcnow(),
|
||||
last_change=now,
|
||||
last_changed_by=user_id,
|
||||
)
|
||||
|
||||
return unarchived
|
||||
@@ -178,12 +224,43 @@ class ModelBLL:
|
||||
"labels_count": {"$size": {"$objectToArray": "$labels"}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$project": {"labels_count": 1},
|
||||
},
|
||||
{"$project": {"labels_count": 1}},
|
||||
]
|
||||
)
|
||||
return {
|
||||
r.pop("_id"): r
|
||||
for r in result
|
||||
return {r.pop("_id"): r for r in result}
|
||||
|
||||
@staticmethod
|
||||
def update_statistics(
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
model_id: str,
|
||||
last_update: datetime = None,
|
||||
last_iteration_max: int = None,
|
||||
last_scalar_events: Dict[str, Dict[str, dict]] = None,
|
||||
):
|
||||
last_update = last_update or datetime.utcnow()
|
||||
updates = {
|
||||
"last_update": datetime.utcnow(),
|
||||
"last_change": last_update,
|
||||
"last_changed_by": user_id,
|
||||
}
|
||||
if last_iteration_max is not None:
|
||||
updates.update(max__last_iteration=last_iteration_max)
|
||||
|
||||
raw_updates = {}
|
||||
if last_scalar_events is not None:
|
||||
raw_updates = {}
|
||||
if last_scalar_events is not None:
|
||||
get_last_metric_updates(
|
||||
task_id=model_id,
|
||||
last_scalar_events=last_scalar_events,
|
||||
raw_updates=raw_updates,
|
||||
extra_updates=updates,
|
||||
model_events=True,
|
||||
)
|
||||
|
||||
ret = Model.objects(id=model_id).update_one(**updates)
|
||||
if ret and raw_updates:
|
||||
Model.objects(id=model_id).update_one(__raw__=[{"$set": raw_updates}])
|
||||
|
||||
return ret
|
||||
|
||||
@@ -5,7 +5,6 @@ from mongoengine import Document
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels.metadata import MetadataItem
|
||||
from apiserver.database.model.base import GetMixin
|
||||
from apiserver.service_repo import APICall
|
||||
from apiserver.utilities.parameter_key_escaper import (
|
||||
ParameterKeyEscaper,
|
||||
mongoengine_safe,
|
||||
@@ -87,13 +86,13 @@ class Metadata:
|
||||
return paths
|
||||
|
||||
@classmethod
|
||||
def escape_query_parameters(cls, call: APICall) -> dict:
|
||||
if not call.data:
|
||||
return call.data
|
||||
def escape_query_parameters(cls, call_data: dict) -> dict:
|
||||
if not call_data:
|
||||
return call_data
|
||||
|
||||
keys = list(call.data)
|
||||
keys = list(call_data)
|
||||
call_data = {
|
||||
safe_key: call.data[key]
|
||||
safe_key: call_data[key]
|
||||
for key, safe_key in zip(keys, Metadata.escape_paths(keys))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
from typing import Sequence, Dict
|
||||
from typing import Sequence, Dict, Type
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.bll.util import update_project_time
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model.model import AttributedDocument
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.redis_manager import redman
|
||||
@@ -22,6 +25,51 @@ class OrgBLL:
|
||||
self._task_tags = _TagsCache(Task, self.redis)
|
||||
self._model_tags = _TagsCache(Model, self.redis)
|
||||
|
||||
def edit_entity_tags(
|
||||
self,
|
||||
company_id,
|
||||
entity_cls: Type[AttributedDocument],
|
||||
entity_ids: Sequence[str],
|
||||
add_tags: Sequence[str],
|
||||
remove_tags: Sequence[str],
|
||||
) -> int:
|
||||
if entity_cls not in (Task, Model):
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Tags editing can be called on tasks or models only"
|
||||
)
|
||||
if not entity_ids:
|
||||
raise errors.bad_request.ValidationError(
|
||||
"No entity ids provided for editing tags"
|
||||
)
|
||||
if not (add_tags or remove_tags):
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Either add tags or remove tags should be provided"
|
||||
)
|
||||
|
||||
updated = 0
|
||||
if add_tags:
|
||||
updated += entity_cls.objects(company=company_id, id__in=entity_ids).update(
|
||||
add_to_set__tags=add_tags
|
||||
)
|
||||
if remove_tags:
|
||||
updated += entity_cls.objects(company=company_id, id__in=entity_ids).update(
|
||||
pull_all__tags=remove_tags
|
||||
)
|
||||
if not updated:
|
||||
return 0
|
||||
|
||||
projects = entity_cls.objects(company=company_id, id__in=entity_ids).distinct(
|
||||
"project"
|
||||
)
|
||||
update_project_time(project_ids=projects)
|
||||
self.update_tags(
|
||||
company_id,
|
||||
entity=Tags.Task if entity_cls is Task else Tags.Model,
|
||||
projects=projects,
|
||||
tags=add_tags or remove_tags
|
||||
)
|
||||
return updated
|
||||
|
||||
def get_tags(
|
||||
self,
|
||||
company_id: str,
|
||||
@@ -50,10 +98,10 @@ class OrgBLL:
|
||||
return ret
|
||||
|
||||
def update_tags(
|
||||
self, company_id: str, entity: Tags, project: str, tags=None, system_tags=None,
|
||||
self, company_id: str, entity: Tags, projects: Sequence[str], tags=None, system_tags=None,
|
||||
):
|
||||
tags_cache = self._get_tags_cache_for_entity(entity)
|
||||
tags_cache.update_tags(company_id, project, tags, system_tags)
|
||||
tags_cache.update_tags(company_id, projects, tags, system_tags)
|
||||
|
||||
def reset_tags(self, company_id: str, entity: Tags, projects: Sequence[str]):
|
||||
tags_cache = self._get_tags_cache_for_entity(entity)
|
||||
|
||||
@@ -107,7 +107,7 @@ class _TagsCache:
|
||||
|
||||
return ret
|
||||
|
||||
def update_tags(self, company_id: str, project: str, tags=None, system_tags=None):
|
||||
def update_tags(self, company_id: str, projects: Sequence[str], tags=None, system_tags=None):
|
||||
"""
|
||||
Updates tags. If reset is set then both tags and system_tags
|
||||
are recalculated. Otherwise only those that are not 'None'
|
||||
@@ -123,7 +123,7 @@ class _TagsCache:
|
||||
if not fields:
|
||||
return
|
||||
|
||||
self._delete_redis_keys(company_id, projects=[project], fields=fields)
|
||||
self._delete_redis_keys(company_id, projects=projects, fields=fields)
|
||||
|
||||
def reset_tags(self, company_id: str, projects: Sequence[str]):
|
||||
self._delete_redis_keys(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from functools import reduce
|
||||
from itertools import groupby
|
||||
from itertools import groupby, chain
|
||||
from operator import itemgetter
|
||||
from typing import (
|
||||
Sequence,
|
||||
@@ -15,15 +14,16 @@ from typing import (
|
||||
Callable,
|
||||
Mapping,
|
||||
Any,
|
||||
Union,
|
||||
)
|
||||
|
||||
from boltons.iterutils import partition
|
||||
from mongoengine import Q, Document
|
||||
|
||||
from apiserver import database
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels.projects import ProjectChildrenType
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model import EntityVisibility, AttributedDocument
|
||||
from apiserver.database.model import EntityVisibility, AttributedDocument, User
|
||||
from apiserver.database.model.base import GetMixin
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.project import Project
|
||||
@@ -40,16 +40,25 @@ from .sub_projects import (
|
||||
_ids_with_children,
|
||||
_ids_with_parents,
|
||||
_get_project_depth,
|
||||
ProjectsChildren,
|
||||
)
|
||||
|
||||
log = config.logger(__file__)
|
||||
max_depth = config.get("services.projects.sub_projects.max_depth", 10)
|
||||
reports_project_name = ".reports"
|
||||
datasets_project_name = ".datasets"
|
||||
pipelines_project_name = ".pipelines"
|
||||
reports_tag = "reports"
|
||||
dataset_tag = "dataset"
|
||||
pipeline_tag = "pipeline"
|
||||
|
||||
|
||||
class ProjectBLL:
|
||||
child_classes = (Task, Model)
|
||||
|
||||
@classmethod
|
||||
def merge_project(
|
||||
cls, company, source_id: str, destination_id: str
|
||||
cls, company: str, source_id: str, destination_id: str
|
||||
) -> Tuple[int, int, Set[str]]:
|
||||
"""
|
||||
Move all the tasks and sub projects from the source project to the destination
|
||||
@@ -81,7 +90,7 @@ class ProjectBLL:
|
||||
)
|
||||
|
||||
moved_entities = 0
|
||||
for entity_type in (Task, Model):
|
||||
for entity_type in cls.child_classes:
|
||||
moved_entities += entity_type.objects(
|
||||
company=company,
|
||||
project=source_id,
|
||||
@@ -297,18 +306,21 @@ class ProjectBLL:
|
||||
"""
|
||||
Move a batch of entities to `project` or a project named `project_name` (create if does not exist)
|
||||
"""
|
||||
project = cls.find_or_create(
|
||||
user=user,
|
||||
company=company,
|
||||
project_id=project,
|
||||
project_name=project_name,
|
||||
description="",
|
||||
)
|
||||
extra = (
|
||||
{"set__last_change": datetime.utcnow()}
|
||||
if hasattr(entity_cls, "last_change")
|
||||
else {}
|
||||
)
|
||||
if project_name or project:
|
||||
project = cls.find_or_create(
|
||||
user=user,
|
||||
company=company,
|
||||
project_id=project,
|
||||
project_name=project_name,
|
||||
description="",
|
||||
)
|
||||
|
||||
extra = {}
|
||||
if hasattr(entity_cls, "last_change"):
|
||||
extra["set__last_change"] = datetime.utcnow()
|
||||
if hasattr(entity_cls, "last_changed_by"):
|
||||
extra["set__last_changed_by"] = user
|
||||
|
||||
entity_cls.objects(company=company, id__in=ids).update(
|
||||
set__project=project, **extra
|
||||
)
|
||||
@@ -394,6 +406,18 @@ class ProjectBLL:
|
||||
"$completed",
|
||||
{"$gt": ["$completed", time_thresh]},
|
||||
additional_cond,
|
||||
{
|
||||
"$not": {
|
||||
"$in": [
|
||||
"$status",
|
||||
[
|
||||
TaskStatus.queued,
|
||||
TaskStatus.in_progress,
|
||||
TaskStatus.failed,
|
||||
],
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
"then": 1,
|
||||
@@ -507,7 +531,7 @@ class ProjectBLL:
|
||||
def aggregate_project_data(
|
||||
func: Callable[[T, T], T],
|
||||
project_ids: Sequence[str],
|
||||
child_projects: Mapping[str, Sequence[Project]],
|
||||
child_projects: ProjectsChildren,
|
||||
data: Mapping[str, T],
|
||||
) -> Dict[str, T]:
|
||||
"""
|
||||
@@ -527,7 +551,10 @@ class ProjectBLL:
|
||||
|
||||
@classmethod
|
||||
def get_dataset_stats(
|
||||
cls, company: str, project_ids: Sequence[str], users: Sequence[str] = None,
|
||||
cls,
|
||||
company: str,
|
||||
project_ids: Sequence[str],
|
||||
users: Sequence[str] = None,
|
||||
) -> Dict[str, dict]:
|
||||
if not project_ids:
|
||||
return {}
|
||||
@@ -559,6 +586,140 @@ class ProjectBLL:
|
||||
for r in Task.aggregate(task_runtime_pipeline)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_projects_children(
|
||||
project_ids: Sequence[str],
|
||||
search_hidden: bool,
|
||||
allowed_ids: Sequence[str],
|
||||
) -> Tuple[ProjectsChildren, Set[str]]:
|
||||
child_projects = _get_sub_projects(
|
||||
project_ids,
|
||||
_only=("id", "name"),
|
||||
search_hidden=search_hidden,
|
||||
allowed_ids=allowed_ids,
|
||||
)
|
||||
return (
|
||||
child_projects,
|
||||
{c.id for c in chain.from_iterable(child_projects.values())},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_children_info(
|
||||
project_ids: Sequence[str], child_projects: ProjectsChildren
|
||||
) -> dict:
|
||||
return {
|
||||
project: sorted(
|
||||
[{"id": c.id, "name": c.name} for c in child_projects.get(project, [])],
|
||||
key=itemgetter("name"),
|
||||
)
|
||||
for project in project_ids
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_project_dataset_stats_core(
|
||||
cls,
|
||||
company: str,
|
||||
project_ids: Sequence[str],
|
||||
project_field: str,
|
||||
entity_class: Type[AttributedDocument],
|
||||
include_children: bool = True,
|
||||
filter_: Mapping[str, Any] = None,
|
||||
users: Sequence[str] = None,
|
||||
selected_project_ids: Sequence[str] = None,
|
||||
) -> Tuple[Dict[str, dict], Dict[str, dict]]:
|
||||
if not project_ids:
|
||||
return {}, {}
|
||||
|
||||
child_projects = {}
|
||||
project_ids_with_children = set(project_ids)
|
||||
if include_children:
|
||||
child_projects, children_ids = cls._get_projects_children(
|
||||
project_ids,
|
||||
search_hidden=True,
|
||||
allowed_ids=selected_project_ids,
|
||||
)
|
||||
project_ids_with_children |= children_ids
|
||||
|
||||
pipeline = [
|
||||
{
|
||||
"$match": cls.get_match_conditions(
|
||||
company=company,
|
||||
project_ids=list(project_ids_with_children),
|
||||
filter_=filter_,
|
||||
users=users,
|
||||
project_field=project_field,
|
||||
)
|
||||
},
|
||||
{"$project": {project_field: 1, "tags": 1}},
|
||||
{
|
||||
"$group": {
|
||||
"_id": f"${project_field}",
|
||||
"count": {"$sum": 1},
|
||||
"tags": {"$push": "$tags"},
|
||||
}
|
||||
},
|
||||
]
|
||||
res = entity_class.aggregate(pipeline)
|
||||
|
||||
project_stats = {
|
||||
result["_id"]: {
|
||||
"count": result.get("count", 0),
|
||||
"tags": set(chain.from_iterable(result.get("tags", []))),
|
||||
}
|
||||
for result in res
|
||||
}
|
||||
|
||||
def concat_dataset_stats(a: dict, b: dict) -> dict:
|
||||
return {
|
||||
"count": a.get("count", 0) + b.get("count", 0),
|
||||
"tags": a.get("tags", {}) | b.get("tags", {}),
|
||||
}
|
||||
|
||||
top_project_stats = cls.aggregate_project_data(
|
||||
func=concat_dataset_stats,
|
||||
project_ids=project_ids,
|
||||
child_projects=child_projects,
|
||||
data=project_stats,
|
||||
)
|
||||
for _, stat in top_project_stats.items():
|
||||
stat["tags"] = sorted(list(stat.get("tags", {})))
|
||||
|
||||
empty_stats = {"count": 0, "tags": []}
|
||||
stats = {
|
||||
project: {"datasets": top_project_stats.get(project, empty_stats)}
|
||||
for project in project_ids
|
||||
}
|
||||
return stats, cls._get_children_info(project_ids, child_projects)
|
||||
|
||||
@classmethod
|
||||
def get_project_dataset_stats(
|
||||
cls,
|
||||
company: str,
|
||||
project_ids: Sequence[str],
|
||||
include_children: bool = True,
|
||||
filter_: Mapping[str, Any] = None,
|
||||
users: Sequence[str] = None,
|
||||
selected_project_ids: Sequence[str] = None,
|
||||
) -> Tuple[Dict[str, dict], Dict[str, dict]]:
|
||||
filter_ = filter_ or {}
|
||||
filter_system_tags = filter_.get("system_tags")
|
||||
if not isinstance(filter_system_tags, list):
|
||||
filter_system_tags = []
|
||||
if dataset_tag not in filter_system_tags:
|
||||
filter_system_tags.append(dataset_tag)
|
||||
filter_["system_tags"] = filter_system_tags
|
||||
|
||||
return cls._get_project_dataset_stats_core(
|
||||
company=company,
|
||||
project_ids=project_ids,
|
||||
project_field="parent",
|
||||
entity_class=Project,
|
||||
include_children=include_children,
|
||||
filter_=filter_,
|
||||
users=users,
|
||||
selected_project_ids=selected_project_ids,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_project_stats(
|
||||
cls,
|
||||
@@ -569,24 +730,21 @@ class ProjectBLL:
|
||||
search_hidden: bool = False,
|
||||
filter_: Mapping[str, Any] = None,
|
||||
users: Sequence[str] = None,
|
||||
user_active_project_ids: Sequence[str] = None,
|
||||
selected_project_ids: Sequence[str] = None,
|
||||
) -> Tuple[Dict[str, dict], Dict[str, dict]]:
|
||||
if not project_ids:
|
||||
return {}, {}
|
||||
|
||||
child_projects = (
|
||||
_get_sub_projects(
|
||||
child_projects = {}
|
||||
project_ids_with_children = set(project_ids)
|
||||
if include_children:
|
||||
child_projects, children_ids = cls._get_projects_children(
|
||||
project_ids,
|
||||
_only=("id", "name"),
|
||||
search_hidden=search_hidden,
|
||||
allowed_ids=user_active_project_ids,
|
||||
allowed_ids=selected_project_ids,
|
||||
)
|
||||
if include_children
|
||||
else {}
|
||||
)
|
||||
project_ids_with_children = set(project_ids) | {
|
||||
c.id for c in itertools.chain.from_iterable(child_projects.values())
|
||||
}
|
||||
project_ids_with_children |= children_ids
|
||||
|
||||
status_count_pipeline, runtime_pipeline = cls.make_projects_get_all_pipelines(
|
||||
company,
|
||||
project_ids=list(project_ids_with_children),
|
||||
@@ -690,14 +848,7 @@ class ProjectBLL:
|
||||
for project in project_ids
|
||||
}
|
||||
|
||||
children = {
|
||||
project: sorted(
|
||||
[{"id": c.id, "name": c.name} for c in child_projects.get(project, [])],
|
||||
key=itemgetter("name"),
|
||||
)
|
||||
for project in project_ids
|
||||
}
|
||||
return stats, children
|
||||
return stats, cls._get_children_info(project_ids, child_projects)
|
||||
|
||||
@classmethod
|
||||
def get_active_users(
|
||||
@@ -722,7 +873,7 @@ class ProjectBLL:
|
||||
projects_query &= Q(id__in=project_ids)
|
||||
|
||||
res = set(Project.objects(projects_query).distinct(field="user"))
|
||||
for cls_ in (Task, Model):
|
||||
for cls_ in cls.child_classes:
|
||||
res |= set(cls_.objects(query).distinct(field="user"))
|
||||
|
||||
return res
|
||||
@@ -751,61 +902,102 @@ class ProjectBLL:
|
||||
return tags, system_tags
|
||||
|
||||
@classmethod
|
||||
def get_projects_with_active_user(
|
||||
def get_projects_with_selected_children(
|
||||
cls,
|
||||
company: str,
|
||||
users: Sequence[str],
|
||||
users: Sequence[str] = None,
|
||||
project_ids: Optional[Sequence[str]] = None,
|
||||
allow_public: bool = True,
|
||||
children_type: ProjectChildrenType = None,
|
||||
children_tags: Sequence[str] = None,
|
||||
children_tags_filter: dict = None,
|
||||
) -> Tuple[Sequence[str], Sequence[str]]:
|
||||
"""
|
||||
Get the projects ids where user created any tasks including all the parents of these projects
|
||||
Get the projects ids matching children_condition (if passed) or where the passed user created any tasks
|
||||
including all the parents of these projects
|
||||
If project ids are specified then filter the results by these project ids
|
||||
"""
|
||||
query = Q(user__in=users)
|
||||
if not (users or children_type):
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Either active users or children_condition should be specified"
|
||||
)
|
||||
|
||||
if allow_public:
|
||||
query &= get_company_or_none_constraint(company)
|
||||
query = (
|
||||
get_company_or_none_constraint(company)
|
||||
if allow_public
|
||||
else Q(company=company)
|
||||
)
|
||||
if users:
|
||||
query &= Q(user__in=users)
|
||||
|
||||
project_query = None
|
||||
if children_tags_filter:
|
||||
child_query = query & GetMixin.get_list_filter_query(
|
||||
"tags", children_tags_filter
|
||||
)
|
||||
elif children_tags:
|
||||
child_query = query & GetMixin.get_list_field_query("tags", children_tags)
|
||||
else:
|
||||
query &= Q(company=company)
|
||||
child_query = query
|
||||
|
||||
if children_type == ProjectChildrenType.dataset:
|
||||
child_queries = {
|
||||
Project: child_query
|
||||
& Q(system_tags__in=[dataset_tag], basename__ne=datasets_project_name)
|
||||
}
|
||||
elif children_type == ProjectChildrenType.pipeline:
|
||||
child_queries = {
|
||||
Project: child_query
|
||||
& Q(system_tags__in=[pipeline_tag], basename__ne=pipelines_project_name)
|
||||
}
|
||||
elif children_type == ProjectChildrenType.report:
|
||||
child_queries = {Task: child_query & Q(system_tags__in=[reports_tag])}
|
||||
else:
|
||||
project_query = query
|
||||
child_queries = {entity_cls: query for entity_cls in cls.child_classes}
|
||||
|
||||
user_projects_query = query
|
||||
if project_ids:
|
||||
ids_with_children = _ids_with_children(project_ids)
|
||||
query &= Q(project__in=ids_with_children)
|
||||
user_projects_query &= Q(id__in=ids_with_children)
|
||||
if project_query:
|
||||
project_query &= Q(id__in=ids_with_children)
|
||||
for child_cls in child_queries:
|
||||
child_queries[child_cls] &= (
|
||||
Q(parent__in=ids_with_children)
|
||||
if child_cls is Project
|
||||
else Q(project__in=ids_with_children)
|
||||
)
|
||||
|
||||
res = {p.id for p in Project.objects(user_projects_query).only("id")}
|
||||
for cls_ in (Task, Model):
|
||||
res |= set(cls_.objects(query).distinct(field="project"))
|
||||
res = (
|
||||
set(Project.objects(project_query).scalar("id")) if project_query else set()
|
||||
)
|
||||
for cls_, query_ in child_queries.items():
|
||||
res |= set(
|
||||
cls_.objects(query_).distinct(
|
||||
field="id" if cls_ is Project else "project"
|
||||
)
|
||||
)
|
||||
|
||||
res = list(res)
|
||||
if not res:
|
||||
return res, res
|
||||
|
||||
user_active_project_ids = _ids_with_parents(res)
|
||||
selected_project_ids = _ids_with_parents(res)
|
||||
filtered_ids = (
|
||||
list(set(user_active_project_ids) & set(project_ids))
|
||||
list(set(selected_project_ids) & set(project_ids))
|
||||
if project_ids
|
||||
else list(user_active_project_ids)
|
||||
else list(selected_project_ids)
|
||||
)
|
||||
|
||||
return filtered_ids, user_active_project_ids
|
||||
return filtered_ids, selected_project_ids
|
||||
|
||||
@classmethod
|
||||
def get_task_parents(
|
||||
cls,
|
||||
company_id: str,
|
||||
projects: Sequence[str],
|
||||
include_subprojects: bool,
|
||||
@staticmethod
|
||||
def _get_project_query(
|
||||
company: str,
|
||||
projects: Sequence,
|
||||
include_subprojects: bool = True,
|
||||
state: Optional[EntityVisibility] = None,
|
||||
) -> Sequence[dict]:
|
||||
"""
|
||||
Get list of unique parent tasks sorted by task name for the passed company projects
|
||||
If projects is None or empty then get parents for all the company tasks
|
||||
"""
|
||||
query = Q(company=company_id)
|
||||
|
||||
) -> Q:
|
||||
query = get_company_or_none_constraint(company)
|
||||
if projects:
|
||||
if include_subprojects:
|
||||
projects = _ids_with_children(projects)
|
||||
@@ -818,6 +1010,25 @@ class ProjectBLL:
|
||||
elif state == EntityVisibility.active:
|
||||
query &= Q(system_tags__nin=[EntityVisibility.archived.value])
|
||||
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
def get_task_parents(
|
||||
cls,
|
||||
company_id: str,
|
||||
projects: Sequence[str],
|
||||
include_subprojects: bool,
|
||||
state: Optional[EntityVisibility] = None,
|
||||
name: str = None,
|
||||
) -> Sequence[dict]:
|
||||
"""
|
||||
Get list of unique parent tasks sorted by task name for the passed company projects
|
||||
If projects is None or empty then get parents for all the company tasks
|
||||
"""
|
||||
query = cls._get_project_query(
|
||||
company_id, projects, include_subprojects=include_subprojects, state=state
|
||||
)
|
||||
|
||||
parent_ids = set(Task.objects(query).distinct("parent"))
|
||||
if not parent_ids:
|
||||
return []
|
||||
@@ -825,23 +1036,37 @@ class ProjectBLL:
|
||||
parents = Task.get_many_with_join(
|
||||
company_id,
|
||||
query=Q(id__in=parent_ids),
|
||||
query_dict={"name": name} if name else None,
|
||||
allow_public=True,
|
||||
override_projection=("id", "name", "project.name"),
|
||||
)
|
||||
|
||||
return sorted(parents, key=itemgetter("name"))
|
||||
|
||||
@classmethod
|
||||
def get_entity_users(
|
||||
cls,
|
||||
company: str,
|
||||
entity_cls: Type[Union[Task, Model]],
|
||||
projects: Sequence[str],
|
||||
include_subprojects: bool,
|
||||
) -> Sequence[dict]:
|
||||
query = cls._get_project_query(
|
||||
company, projects, include_subprojects=include_subprojects
|
||||
)
|
||||
user_ids = entity_cls.objects(query).distinct(field="user")
|
||||
if not user_ids:
|
||||
return []
|
||||
users = User.objects(id__in=user_ids).only("id", "name")
|
||||
return [{"id": u.id, "name": u.name} for u in users]
|
||||
|
||||
@classmethod
|
||||
def get_task_types(cls, company, project_ids: Optional[Sequence]) -> set:
|
||||
"""
|
||||
Return the list of unique task types used by company and public tasks
|
||||
If project ids passed then only tasks from these projects are considered
|
||||
"""
|
||||
query = get_company_or_none_constraint(company)
|
||||
if project_ids:
|
||||
project_ids = _ids_with_children(project_ids)
|
||||
query &= Q(project__in=project_ids)
|
||||
else:
|
||||
query &= Q(system_tags__nin=[EntityVisibility.hidden.value])
|
||||
query = cls._get_project_query(company, project_ids)
|
||||
res = Task.objects(query).distinct(field="type")
|
||||
return set(res).intersection(external_task_types)
|
||||
|
||||
@@ -851,10 +1076,7 @@ class ProjectBLL:
|
||||
Return the list of unique frameworks used by company and public models
|
||||
If project ids passed then only models from these projects are considered
|
||||
"""
|
||||
query = get_company_or_none_constraint(company)
|
||||
if project_ids:
|
||||
project_ids = _ids_with_children(project_ids)
|
||||
query &= Q(project__in=project_ids)
|
||||
query = cls._get_project_query(company, project_ids)
|
||||
return Model.objects(query).distinct(field="framework")
|
||||
|
||||
@staticmethod
|
||||
@@ -863,10 +1085,11 @@ class ProjectBLL:
|
||||
project_ids: Sequence[str],
|
||||
filter_: Mapping[str, Any],
|
||||
users: Sequence[str],
|
||||
project_field: str = "project",
|
||||
):
|
||||
conditions = {
|
||||
"company": {"$in": [None, "", company]},
|
||||
"project": {"$in": project_ids},
|
||||
project_field: {"$in": project_ids},
|
||||
}
|
||||
if users:
|
||||
conditions["user"] = {"$in": users}
|
||||
@@ -874,29 +1097,129 @@ class ProjectBLL:
|
||||
if not filter_:
|
||||
return conditions
|
||||
|
||||
or_conditions = []
|
||||
for field, field_filter in filter_.items():
|
||||
if not (
|
||||
field_filter
|
||||
and isinstance(field_filter, list)
|
||||
and all(isinstance(t, str) for t in field_filter)
|
||||
):
|
||||
if not (field_filter and isinstance(field_filter, (list, dict))):
|
||||
raise errors.bad_request.ValidationError(
|
||||
f"List of strings expected for the field: {field}"
|
||||
f"Non empty list or dictionary expected for the field: {field}"
|
||||
)
|
||||
exclude, include = partition(field_filter, lambda x: x.startswith("-"))
|
||||
conditions[field] = {
|
||||
**({"$in": include} if include else {}),
|
||||
**({"$nin": [e[1:] for e in exclude]} if exclude else {}),
|
||||
}
|
||||
|
||||
if isinstance(field_filter, list):
|
||||
if not all(isinstance(t, str) for t in field_filter):
|
||||
raise errors.bad_request.ValidationError(
|
||||
f"Only string values are allowed in the list filter: {field}"
|
||||
)
|
||||
helper = GetMixin.NewListFieldBucketHelper(
|
||||
field, data=field_filter, legacy=True
|
||||
)
|
||||
op = (
|
||||
Q.OR
|
||||
if helper.explicit_operator and helper.global_operator == Q.OR
|
||||
else Q.AND
|
||||
)
|
||||
db_query = {op: helper.actions}
|
||||
else:
|
||||
helper = GetMixin.ListQueryFilter.from_data(field, field_filter)
|
||||
db_query = helper.db_query
|
||||
|
||||
for op, actions in db_query.items():
|
||||
field_conditions = {}
|
||||
for action, values in actions.items():
|
||||
value = list(set(values))
|
||||
for key in reversed(action.split("__")):
|
||||
value = {f"${key}": value}
|
||||
field_conditions.update(value)
|
||||
|
||||
if op == Q.OR and len(field_conditions) > 1:
|
||||
or_conditions.append(
|
||||
{
|
||||
"$or": [
|
||||
{field: {db_modifier: cond}}
|
||||
for db_modifier, cond in field_conditions.items()
|
||||
]
|
||||
}
|
||||
)
|
||||
else:
|
||||
conditions[field] = field_conditions
|
||||
|
||||
if or_conditions:
|
||||
if len(or_conditions) == 1:
|
||||
conditions = next(iter(or_conditions))
|
||||
else:
|
||||
conditions["$and"] = [c for c in or_conditions]
|
||||
|
||||
return conditions
|
||||
|
||||
@classmethod
|
||||
def _calc_own_datasets_core(
|
||||
cls,
|
||||
company: str,
|
||||
project_ids: Sequence[str],
|
||||
project_field: str,
|
||||
entity_class: Type[AttributedDocument],
|
||||
filter_: Mapping[str, Any] = None,
|
||||
users: Sequence[str] = None,
|
||||
) -> Dict[str, dict]:
|
||||
"""
|
||||
Returns the amount of hyper datasets per requested project
|
||||
"""
|
||||
if not project_ids:
|
||||
return {}
|
||||
|
||||
pipeline = [
|
||||
{
|
||||
"$match": cls.get_match_conditions(
|
||||
company=company,
|
||||
project_ids=project_ids,
|
||||
filter_=filter_,
|
||||
users=users,
|
||||
project_field=project_field,
|
||||
)
|
||||
},
|
||||
{"$project": {project_field: 1}},
|
||||
{"$group": {"_id": f"${project_field}", "count": {"$sum": 1}}},
|
||||
]
|
||||
datasets = {
|
||||
data["_id"]: data["count"] for data in entity_class.aggregate(pipeline)
|
||||
}
|
||||
|
||||
return {pid: {"own_datasets": datasets.get(pid, 0)} for pid in project_ids}
|
||||
|
||||
@classmethod
|
||||
def calc_own_datasets(
|
||||
cls,
|
||||
company: str,
|
||||
project_ids: Sequence[str],
|
||||
filter_: Mapping[str, Any] = None,
|
||||
users: Sequence[str] = None,
|
||||
) -> Dict[str, dict]:
|
||||
"""
|
||||
Returns the amount of datasets per requested project
|
||||
"""
|
||||
filter_ = filter_ or {}
|
||||
filter_system_tags = filter_.get("system_tags")
|
||||
if not isinstance(filter_system_tags, list):
|
||||
filter_system_tags = []
|
||||
if dataset_tag not in filter_system_tags:
|
||||
filter_system_tags.append(dataset_tag)
|
||||
filter_["system_tags"] = filter_system_tags
|
||||
|
||||
return cls._calc_own_datasets_core(
|
||||
company=company,
|
||||
project_ids=project_ids,
|
||||
project_field="parent",
|
||||
entity_class=Project,
|
||||
filter_=filter_,
|
||||
users=users,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def calc_own_contents(
|
||||
cls,
|
||||
company: str,
|
||||
project_ids: Sequence[str],
|
||||
filter_: Mapping[str, Any] = None,
|
||||
specific_state: Optional[EntityVisibility] = None,
|
||||
users: Sequence[str] = None,
|
||||
) -> Dict[str, dict]:
|
||||
"""
|
||||
@@ -907,6 +1230,20 @@ class ProjectBLL:
|
||||
if not project_ids:
|
||||
return {}
|
||||
|
||||
if specific_state:
|
||||
filter_ = filter_ or {}
|
||||
system_tags_filter = filter_.get("system_tags", [])
|
||||
archived = EntityVisibility.archived.value
|
||||
non_archived = f"-{EntityVisibility.archived.value}"
|
||||
if not any(t in system_tags_filter for t in (archived, non_archived)):
|
||||
filter_ = {k: v for k, v in filter_.items()}
|
||||
filter_["system_tags"] = [
|
||||
archived
|
||||
if specific_state == EntityVisibility.archived
|
||||
else non_archived,
|
||||
*system_tags_filter,
|
||||
]
|
||||
|
||||
pipeline = [
|
||||
{
|
||||
"$match": cls.get_match_conditions(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Set, Sequence
|
||||
|
||||
import attr
|
||||
from mongoengine import Q
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.bll.event import EventBLL
|
||||
@@ -14,12 +17,19 @@ 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, TaskType
|
||||
from apiserver.database.model.task.task import Task, ArtifactModes, TaskType, TaskStatus
|
||||
from .project_bll import (
|
||||
ProjectBLL,
|
||||
pipeline_tag,
|
||||
pipelines_project_name,
|
||||
dataset_tag,
|
||||
datasets_project_name,
|
||||
reports_tag,
|
||||
)
|
||||
from .sub_projects import _ids_with_children
|
||||
|
||||
log = config.logger(__file__)
|
||||
event_bll = EventBLL()
|
||||
async_events_delete = config.get("services.tasks.async_events_delete", False)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@@ -31,30 +41,83 @@ class DeleteProjectResult:
|
||||
urls: TaskUrls = None
|
||||
|
||||
|
||||
def _get_child_project_ids(
|
||||
project_id: str,
|
||||
) -> Tuple[Sequence[str], Sequence[str], Sequence[str]]:
|
||||
project_ids = _ids_with_children([project_id])
|
||||
pipeline_ids = list(
|
||||
Project.objects(
|
||||
id__in=project_ids,
|
||||
system_tags__in=[pipeline_tag],
|
||||
basename__ne=pipelines_project_name,
|
||||
).scalar("id")
|
||||
)
|
||||
dataset_ids = list(
|
||||
Project.objects(
|
||||
id__in=project_ids,
|
||||
system_tags__in=[dataset_tag],
|
||||
basename__ne=datasets_project_name,
|
||||
).scalar("id")
|
||||
)
|
||||
return project_ids, pipeline_ids, dataset_ids
|
||||
|
||||
|
||||
def validate_project_delete(company: str, project_id: str):
|
||||
project = Project.get_for_writing(
|
||||
company=company, id=project_id, _only=("id", "path", "system_tags")
|
||||
)
|
||||
if not project:
|
||||
raise errors.bad_request.InvalidProjectId(id=project_id)
|
||||
is_pipeline = "pipeline" in (project.system_tags or [])
|
||||
project_ids = _ids_with_children([project_id])
|
||||
|
||||
project_ids, pipeline_ids, dataset_ids = _get_child_project_ids(project_id)
|
||||
ret = {}
|
||||
for cls in (Task, Model):
|
||||
ret[f"{cls.__name__.lower()}s"] = cls.objects(project__in=project_ids).count()
|
||||
for cls in (Task, Model):
|
||||
query = dict(
|
||||
project__in=project_ids, system_tags__nin=[EntityVisibility.archived.value]
|
||||
)
|
||||
name = f"non_archived_{cls.__name__.lower()}s"
|
||||
if not is_pipeline:
|
||||
ret[name] = cls.objects(**query).count()
|
||||
else:
|
||||
ret[name] = (
|
||||
cls.objects(**query, type=TaskType.controller).count()
|
||||
if cls == Task
|
||||
else 0
|
||||
if pipeline_ids:
|
||||
pipelines_with_active_controllers = Task.objects(
|
||||
project__in=pipeline_ids,
|
||||
type=TaskType.controller,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).distinct("project")
|
||||
ret["pipelines"] = len(pipelines_with_active_controllers)
|
||||
else:
|
||||
ret["pipelines"] = 0
|
||||
if dataset_ids:
|
||||
datasets_with_data = Task.objects(
|
||||
project__in=dataset_ids,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).distinct("project")
|
||||
ret["datasets"] = len(datasets_with_data)
|
||||
else:
|
||||
ret["datasets"] = 0
|
||||
|
||||
project_ids = list(set(project_ids) - set(pipeline_ids) - set(dataset_ids))
|
||||
if project_ids:
|
||||
in_project_query = Q(project__in=project_ids)
|
||||
for cls in (Task, Model):
|
||||
query = (
|
||||
in_project_query & Q(system_tags__nin=[reports_tag])
|
||||
if cls is Task
|
||||
else in_project_query
|
||||
)
|
||||
ret[f"{cls.__name__.lower()}s"] = cls.objects(query).count()
|
||||
ret[f"non_archived_{cls.__name__.lower()}s"] = cls.objects(
|
||||
query & Q(system_tags__nin=[EntityVisibility.archived.value])
|
||||
).count()
|
||||
ret["reports"] = Task.objects(
|
||||
in_project_query & Q(system_tags__in=[reports_tag])
|
||||
).count()
|
||||
ret["non_archived_reports"] = Task.objects(
|
||||
in_project_query
|
||||
& Q(
|
||||
system_tags__in=[reports_tag],
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
)
|
||||
).count()
|
||||
else:
|
||||
for cls in (Task, Model):
|
||||
ret[f"{cls.__name__.lower()}s"] = 0
|
||||
ret[f"non_archived_{cls.__name__.lower()}s"] = 0
|
||||
ret["reports"] = 0
|
||||
ret["non_archived_reports"] = 0
|
||||
|
||||
return ret
|
||||
|
||||
@@ -65,7 +128,7 @@ def delete_project(
|
||||
project_id: str,
|
||||
force: bool,
|
||||
delete_contents: bool,
|
||||
delete_external_artifacts=True,
|
||||
delete_external_artifacts: bool,
|
||||
) -> Tuple[DeleteProjectResult, Set[str]]:
|
||||
project = Project.get_for_writing(
|
||||
company=company, id=project_id, _only=("id", "path", "system_tags")
|
||||
@@ -74,39 +137,58 @@ def delete_project(
|
||||
raise errors.bad_request.InvalidProjectId(id=project_id)
|
||||
|
||||
delete_external_artifacts = delete_external_artifacts and config.get(
|
||||
"services.async_urls_delete.enabled", False
|
||||
"services.async_urls_delete.enabled", True
|
||||
)
|
||||
is_pipeline = "pipeline" in (project.system_tags or [])
|
||||
project_ids = _ids_with_children([project_id])
|
||||
project_ids, pipeline_ids, dataset_ids = _get_child_project_ids(project_id)
|
||||
if not force:
|
||||
query = dict(
|
||||
project__in=project_ids, system_tags__nin=[EntityVisibility.archived.value]
|
||||
)
|
||||
if not is_pipeline:
|
||||
if pipeline_ids:
|
||||
active_controllers = Task.objects(
|
||||
project__in=pipeline_ids,
|
||||
type=TaskType.controller,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).only("id")
|
||||
if active_controllers:
|
||||
raise errors.bad_request.ProjectHasPipelines(
|
||||
"please archive all the controllers or use force=true",
|
||||
id=project_id,
|
||||
)
|
||||
if dataset_ids:
|
||||
datasets_with_data = Task.objects(
|
||||
project__in=dataset_ids,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).only("id")
|
||||
if datasets_with_data:
|
||||
raise errors.bad_request.ProjectHasDatasets(
|
||||
"please delete all the dataset versions or use force=true",
|
||||
id=project_id,
|
||||
)
|
||||
|
||||
regular_projects = list(set(project_ids) - set(pipeline_ids) - set(dataset_ids))
|
||||
if regular_projects:
|
||||
for cls, error in (
|
||||
(Task, errors.bad_request.ProjectHasTasks),
|
||||
(Model, errors.bad_request.ProjectHasModels),
|
||||
):
|
||||
non_archived = cls.objects(**query).only("id")
|
||||
non_archived = cls.objects(
|
||||
project__in=regular_projects,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).only("id")
|
||||
if non_archived:
|
||||
raise error("use force=true to delete", id=project_id)
|
||||
else:
|
||||
non_archived = Task.objects(**query, type=TaskType.controller).only("id")
|
||||
if non_archived:
|
||||
raise errors.bad_request.ProjectHasTasks(
|
||||
"please archive all the runs inside the project", id=project_id
|
||||
)
|
||||
raise error("use force=true", id=project_id)
|
||||
|
||||
if not delete_contents:
|
||||
for cls in (Model, Task):
|
||||
updated_count = cls.objects(project__in=project_ids).update(project=None)
|
||||
res = DeleteProjectResult(disassociated_tasks=updated_count)
|
||||
disassociated = defaultdict(int)
|
||||
for cls in ProjectBLL.child_classes:
|
||||
disassociated[cls] = cls.objects(project__in=project_ids).update(
|
||||
project=None
|
||||
)
|
||||
res = DeleteProjectResult(disassociated_tasks=disassociated[Task])
|
||||
else:
|
||||
deleted_models, model_event_urls, model_urls = _delete_models(
|
||||
company=company, projects=project_ids
|
||||
company=company, user=user, projects=project_ids
|
||||
)
|
||||
deleted_tasks, task_event_urls, artifact_urls = _delete_tasks(
|
||||
company=company, projects=project_ids
|
||||
company=company, user=user, projects=project_ids
|
||||
)
|
||||
event_urls = task_event_urls | model_event_urls
|
||||
if delete_external_artifacts:
|
||||
@@ -135,7 +217,9 @@ def delete_project(
|
||||
return res, affected
|
||||
|
||||
|
||||
def _delete_tasks(company: str, projects: Sequence[str]) -> Tuple[int, Set, Set]:
|
||||
def _delete_tasks(
|
||||
company: str, user: str, projects: Sequence[str]
|
||||
) -> Tuple[int, Set, Set]:
|
||||
"""
|
||||
Delete only the task themselves and their non published version.
|
||||
Child models under the same project are deleted separately.
|
||||
@@ -146,14 +230,24 @@ def _delete_tasks(company: str, projects: Sequence[str]) -> Tuple[int, Set, Set]
|
||||
if not tasks:
|
||||
return 0, set(), set()
|
||||
|
||||
task_ids = {t.id for t in tasks}
|
||||
Task.objects(parent__in=task_ids, project__nin=projects).update(parent=None)
|
||||
Model.objects(task__in=task_ids, project__nin=projects).update(task=None)
|
||||
task_ids = list({t.id for t in tasks})
|
||||
now = datetime.utcnow()
|
||||
Task.objects(parent__in=task_ids, project__nin=projects).update(
|
||||
parent=None,
|
||||
last_change=now,
|
||||
last_changed_by=user,
|
||||
)
|
||||
Model.objects(task__in=task_ids, project__nin=projects).update(
|
||||
task=None,
|
||||
last_change=now,
|
||||
last_changed_by=user,
|
||||
)
|
||||
|
||||
event_urls, artifact_urls = set(), set()
|
||||
event_urls = collect_debug_image_urls(company, task_ids) | collect_plot_image_urls(
|
||||
company, task_ids
|
||||
)
|
||||
artifact_urls = 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(
|
||||
{
|
||||
@@ -163,15 +257,13 @@ def _delete_tasks(company: str, projects: Sequence[str]) -> Tuple[int, Set, Set]
|
||||
}
|
||||
)
|
||||
|
||||
event_bll.delete_multi_task_events(
|
||||
company, list(task_ids), async_delete=async_events_delete
|
||||
)
|
||||
event_bll.delete_multi_task_events(company, task_ids)
|
||||
deleted = tasks.delete()
|
||||
return deleted, event_urls, artifact_urls
|
||||
|
||||
|
||||
def _delete_models(
|
||||
company: str, projects: Sequence[str]
|
||||
company: str, user: str, projects: Sequence[str]
|
||||
) -> Tuple[int, Set[str], Set[str]]:
|
||||
"""
|
||||
Delete project models and update the tasks from other projects
|
||||
@@ -182,39 +274,54 @@ def _delete_models(
|
||||
return 0, set(), set()
|
||||
|
||||
model_ids = list({m.id for m in models})
|
||||
|
||||
deleted = "__DELETED__"
|
||||
Task._get_collection().update_many(
|
||||
filter={
|
||||
"project": {"$nin": projects},
|
||||
"models.input.model": {"$in": model_ids},
|
||||
},
|
||||
update={"$set": {"models.input.$[elem].model": None}},
|
||||
update={"$set": {"models.input.$[elem].model": deleted}},
|
||||
array_filters=[{"elem.model": {"$in": model_ids}}],
|
||||
upsert=False,
|
||||
)
|
||||
|
||||
model_tasks = list({m.task for m in models if m.task})
|
||||
if model_tasks:
|
||||
now = datetime.utcnow()
|
||||
# update published tasks
|
||||
Task._get_collection().update_many(
|
||||
filter={
|
||||
"_id": {"$in": model_tasks},
|
||||
"project": {"$nin": projects},
|
||||
"models.output.model": {"$in": model_ids},
|
||||
"status": TaskStatus.published,
|
||||
},
|
||||
update={
|
||||
"$set": {
|
||||
"models.output.$[elem].model": deleted,
|
||||
"last_change": now,
|
||||
"last_changed_by": user,
|
||||
}
|
||||
},
|
||||
update={"$set": {"models.output.$[elem].model": None}},
|
||||
array_filters=[{"elem.model": {"$in": model_ids}}],
|
||||
upsert=False,
|
||||
)
|
||||
# update unpublished tasks
|
||||
Task.objects(
|
||||
id__in=model_tasks,
|
||||
project__nin=projects,
|
||||
status__ne=TaskStatus.published,
|
||||
).update(
|
||||
pull__models__output__model__in=model_ids,
|
||||
set__last_change=now,
|
||||
set__last_changed_by=user,
|
||||
)
|
||||
|
||||
event_urls, model_urls = set(), set()
|
||||
for m in models:
|
||||
event_urls.update(collect_debug_image_urls(company, m.id))
|
||||
event_urls.update(collect_plot_image_urls(company, m.id))
|
||||
if m.uri:
|
||||
model_urls.add(m.uri)
|
||||
|
||||
event_bll.delete_multi_task_events(
|
||||
company, model_ids, async_delete=async_events_delete
|
||||
event_urls = collect_debug_image_urls(company, model_ids) | collect_plot_image_urls(
|
||||
company, model_ids
|
||||
)
|
||||
model_urls = {m.uri for m in models if m.uri}
|
||||
|
||||
event_bll.delete_multi_task_events(company, model_ids, model=True)
|
||||
deleted = models.delete()
|
||||
return deleted, event_urls, model_urls
|
||||
|
||||
@@ -140,7 +140,12 @@ class ProjectQueries:
|
||||
name: str,
|
||||
include_subprojects: bool,
|
||||
allow_public: bool = True,
|
||||
pattern: str = None,
|
||||
page: int = 0,
|
||||
page_size: int = 500,
|
||||
) -> ParamValues:
|
||||
page = max(0, page)
|
||||
page_size = max(1, page_size)
|
||||
company_constraint = self._get_company_constraint(company_id, allow_public)
|
||||
project_constraint = self._get_project_constraint(
|
||||
project_ids, include_subprojects
|
||||
@@ -160,7 +165,20 @@ class ProjectQueries:
|
||||
if not last_updated_task:
|
||||
return 0, []
|
||||
|
||||
redis_key = f"hyperparam_values_{company_id}_{'_'.join(project_ids)}_{section}_{name}_{allow_public}"
|
||||
redis_key = "_".join(
|
||||
str(part)
|
||||
for part in (
|
||||
"hyperparam_values",
|
||||
company_id,
|
||||
"_".join(project_ids),
|
||||
section,
|
||||
name,
|
||||
allow_public,
|
||||
pattern,
|
||||
page,
|
||||
page_size,
|
||||
)
|
||||
)
|
||||
last_update = last_updated_task.last_update or datetime.utcnow()
|
||||
cached_res = self._get_cached_param_values(
|
||||
key=redis_key,
|
||||
@@ -172,19 +190,27 @@ class ProjectQueries:
|
||||
if cached_res:
|
||||
return cached_res
|
||||
|
||||
max_values = config.get("services.tasks.hyperparam_values.max_count", 100)
|
||||
pipeline = [
|
||||
{
|
||||
"$match": {
|
||||
**company_constraint,
|
||||
**project_constraint,
|
||||
key_path: {"$exists": True},
|
||||
match_condition = {
|
||||
**company_constraint,
|
||||
**project_constraint,
|
||||
key_path: {"$exists": True},
|
||||
}
|
||||
if pattern:
|
||||
match_condition["$expr"] = {
|
||||
"$regexMatch": {
|
||||
"input": f"${key_path}.value",
|
||||
"regex": pattern,
|
||||
"options": "i",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
pipeline = [
|
||||
{"$match": match_condition},
|
||||
{"$project": {"value": f"${key_path}.value"}},
|
||||
{"$group": {"_id": "$value"}},
|
||||
{"$sort": {"_id": 1}},
|
||||
{"$limit": max_values},
|
||||
{"$skip": page * page_size},
|
||||
{"$limit": page_size},
|
||||
{
|
||||
"$group": {
|
||||
"_id": 1,
|
||||
@@ -209,7 +235,11 @@ class ProjectQueries:
|
||||
|
||||
@classmethod
|
||||
def get_unique_metric_variants(
|
||||
cls, company_id, project_ids: Sequence[str], include_subprojects: bool
|
||||
cls,
|
||||
company_id,
|
||||
project_ids: Sequence[str],
|
||||
include_subprojects: bool,
|
||||
model_metrics: bool = False,
|
||||
):
|
||||
pipeline = [
|
||||
{
|
||||
@@ -246,7 +276,8 @@ class ProjectQueries:
|
||||
{"$sort": OrderedDict({"_id.metric": 1, "_id.variant": 1})},
|
||||
]
|
||||
|
||||
result = Task.aggregate(pipeline)
|
||||
entity_cls = Model if model_metrics else Task
|
||||
result = entity_cls.aggregate(pipeline)
|
||||
return [r["metrics"][0] for r in result]
|
||||
|
||||
@classmethod
|
||||
@@ -306,7 +337,11 @@ class ProjectQueries:
|
||||
key: str,
|
||||
include_subprojects: bool,
|
||||
allow_public: bool = True,
|
||||
page: int = 0,
|
||||
page_size: int = 500,
|
||||
) -> ParamValues:
|
||||
page = max(0, page)
|
||||
page_size = max(1, page_size)
|
||||
company_constraint = self._get_company_constraint(company_id, allow_public)
|
||||
project_constraint = self._get_project_constraint(
|
||||
project_ids, include_subprojects
|
||||
@@ -326,7 +361,7 @@ class ProjectQueries:
|
||||
if not last_updated_model:
|
||||
return 0, []
|
||||
|
||||
redis_key = f"modelmetadata_values_{company_id}_{'_'.join(project_ids)}_{key}_{allow_public}"
|
||||
redis_key = f"modelmetadata_values_{company_id}_{'_'.join(project_ids)}_{key}_{allow_public}_{page}_{page_size}"
|
||||
last_update = last_updated_model.last_update or datetime.utcnow()
|
||||
cached_res = self._get_cached_param_values(
|
||||
key=redis_key, last_update=last_update
|
||||
@@ -334,7 +369,6 @@ class ProjectQueries:
|
||||
if cached_res:
|
||||
return cached_res
|
||||
|
||||
max_values = config.get("services.models.metadata_values.max_count", 100)
|
||||
pipeline = [
|
||||
{
|
||||
"$match": {
|
||||
@@ -346,7 +380,8 @@ class ProjectQueries:
|
||||
{"$project": {"value": f"${key_path}.value"}},
|
||||
{"$group": {"_id": "$value"}},
|
||||
{"$sort": {"_id": 1}},
|
||||
{"$limit": max_values},
|
||||
{"$skip": page * page_size},
|
||||
{"$limit": page_size},
|
||||
{
|
||||
"$group": {
|
||||
"_id": 1,
|
||||
|
||||
@@ -14,14 +14,16 @@ def _get_project_depth(project_name: str) -> int:
|
||||
return len(list(filter(None, project_name.split(name_separator))))
|
||||
|
||||
|
||||
def _validate_project_name(project_name: str) -> Tuple[str, str]:
|
||||
def _validate_project_name(project_name: str, raise_if_empty=True) -> Tuple[str, str]:
|
||||
"""
|
||||
Remove redundant '/' characters. Ensure that the project name is not empty
|
||||
Return the cleaned up project name and location
|
||||
"""
|
||||
name_parts = list(filter(None, project_name.split(name_separator)))
|
||||
name_parts = [p.strip() for p in project_name.split(name_separator) if p]
|
||||
if not name_parts:
|
||||
raise errors.bad_request.InvalidProjectName(name=project_name)
|
||||
if raise_if_empty:
|
||||
raise errors.bad_request.InvalidProjectName(name=project_name)
|
||||
return "", ""
|
||||
|
||||
return name_separator.join(name_parts), name_separator.join(name_parts[:-1])
|
||||
|
||||
@@ -34,7 +36,7 @@ def _ensure_project(
|
||||
If needed auto-create the project and all the missing projects in the path to it
|
||||
Return the project
|
||||
"""
|
||||
name = name.strip(name_separator)
|
||||
name, location = _validate_project_name(name, raise_if_empty=False)
|
||||
if not name:
|
||||
return None
|
||||
|
||||
@@ -43,7 +45,6 @@ def _ensure_project(
|
||||
return project
|
||||
|
||||
now = datetime.utcnow()
|
||||
name, location = _validate_project_name(name)
|
||||
project = Project(
|
||||
id=database.utils.id(),
|
||||
user=user,
|
||||
@@ -101,12 +102,15 @@ def _get_writable_project_from_name(
|
||||
return qs.first()
|
||||
|
||||
|
||||
ProjectsChildren = Mapping[str, Sequence[Project]]
|
||||
|
||||
|
||||
def _get_sub_projects(
|
||||
project_ids: Sequence[str],
|
||||
_only: Sequence[str] = ("id", "path"),
|
||||
search_hidden=True,
|
||||
allowed_ids: Sequence[str] = None,
|
||||
) -> Mapping[str, Sequence[Project]]:
|
||||
) -> ProjectsChildren:
|
||||
"""
|
||||
Return the list of child projects of all the levels for the parent project ids
|
||||
"""
|
||||
@@ -140,8 +144,8 @@ def _ids_with_children(project_ids: Sequence[str]) -> Sequence[str]:
|
||||
"""
|
||||
Return project ids with the ids of all the subprojects
|
||||
"""
|
||||
subprojects = Project.objects(path__in=project_ids).only("id")
|
||||
return list({*project_ids, *(child.id for child in subprojects)})
|
||||
children_ids = Project.objects(path__in=project_ids).scalar("id")
|
||||
return list({*project_ids, *children_ids})
|
||||
|
||||
|
||||
def _update_subproject_names(
|
||||
@@ -156,13 +160,17 @@ def _update_subproject_names(
|
||||
Optionally update the paths
|
||||
"""
|
||||
updated = 0
|
||||
now = datetime.utcnow()
|
||||
for child in children:
|
||||
child_suffix = name_separator.join(
|
||||
child.name.split(name_separator)[len(old_name.split(name_separator)) :]
|
||||
child.name.split(name_separator)[len(old_name.split(name_separator)):]
|
||||
)
|
||||
updates = {"name": name_separator.join((project.name, child_suffix))}
|
||||
updates = {
|
||||
"name": name_separator.join((project.name, child_suffix)),
|
||||
"last_update": now,
|
||||
}
|
||||
if update_path:
|
||||
updates["path"] = project.path + child.path[len(old_path) :]
|
||||
updates["path"] = project.path + child.path[len(old_path):]
|
||||
updated += child.update(upsert=False, **updates)
|
||||
|
||||
return updated
|
||||
@@ -177,6 +185,7 @@ def _reposition_project_with_children(
|
||||
project.name = name_separator.join(
|
||||
filter(None, (new_location, project.name.split(name_separator)[-1]))
|
||||
)
|
||||
project.last_update = datetime.utcnow()
|
||||
_save_under_parent(project, parent=parent)
|
||||
|
||||
moved = 1 + _update_subproject_names(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Callable, Sequence, Optional, Tuple
|
||||
from typing import Sequence, Optional, Tuple, Union
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from mongoengine import Q
|
||||
@@ -16,6 +16,8 @@ from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.queue import Queue, Entry
|
||||
|
||||
log = config.logger(__file__)
|
||||
MOVE_FIRST = "first"
|
||||
MOVE_LAST = "last"
|
||||
|
||||
|
||||
class QueueBLL(object):
|
||||
@@ -133,7 +135,7 @@ class QueueBLL(object):
|
||||
self.get_by_id(company_id=company_id, queue_id=queue_id, only=("id",))
|
||||
return Queue.safe_update(company_id, queue_id, update_fields)
|
||||
|
||||
def delete(self, company_id: str, queue_id: str, force: bool) -> None:
|
||||
def delete(self, company_id: str, user_id: str, queue_id: str, force: bool) -> None:
|
||||
"""
|
||||
Delete the queue
|
||||
:raise errors.bad_request.InvalidQueueId: if the queue is not found
|
||||
@@ -153,7 +155,13 @@ class QueueBLL(object):
|
||||
task = Task.get_for_writing(
|
||||
company=company_id,
|
||||
id=item.task,
|
||||
_only=["id", "status", "enqueue_status", "project"],
|
||||
_only=[
|
||||
"id",
|
||||
"company",
|
||||
"status",
|
||||
"enqueue_status",
|
||||
"project",
|
||||
],
|
||||
)
|
||||
if not task:
|
||||
continue
|
||||
@@ -163,6 +171,8 @@ class QueueBLL(object):
|
||||
new_status=task.enqueue_status or TaskStatus.created,
|
||||
status_reason="Queue was deleted",
|
||||
status_message="",
|
||||
user_id=user_id,
|
||||
force=True,
|
||||
).execute(enqueue_status=None)
|
||||
except Exception as ex:
|
||||
log.exception(
|
||||
@@ -233,6 +243,7 @@ class QueueBLL(object):
|
||||
{
|
||||
"name": w.id,
|
||||
"ip": w.ip,
|
||||
"key": w.key,
|
||||
"task": w.task.to_struct() if w.task else None,
|
||||
}
|
||||
for w in queue_workers.get(item["id"], [])
|
||||
@@ -318,46 +329,131 @@ class QueueBLL(object):
|
||||
return len(entries_to_remove) if res else 0
|
||||
|
||||
def reposition_task(
|
||||
self,
|
||||
company_id: str,
|
||||
queue_id: str,
|
||||
task_id: str,
|
||||
pos_func: Callable[[int], int],
|
||||
self, company_id: str, queue_id: str, task_id: str, move_count: Union[int, str],
|
||||
) -> int:
|
||||
"""
|
||||
Moves the task in the queue to the position calculated by pos_func
|
||||
Returns the updated task position in the queue
|
||||
"""
|
||||
with translate_errors_context():
|
||||
queue = self.get_queue_with_task(
|
||||
|
||||
def get_queue_and_task_position():
|
||||
q = self.get_queue_with_task(
|
||||
company_id=company_id, queue_id=queue_id, task_id=task_id
|
||||
)
|
||||
return q, next(i for i, e in enumerate(q.entries) if e.task == task_id)
|
||||
|
||||
position = next(i for i, e in enumerate(queue.entries) if e.task == task_id)
|
||||
new_position = pos_func(position)
|
||||
with translate_errors_context():
|
||||
queue, position = get_queue_and_task_position()
|
||||
if move_count == MOVE_FIRST:
|
||||
new_position = 0
|
||||
elif move_count == MOVE_LAST:
|
||||
new_position = len(queue.entries) - 1
|
||||
else:
|
||||
new_position = position + move_count
|
||||
if new_position == position:
|
||||
return new_position
|
||||
|
||||
if new_position != position:
|
||||
entry = queue.entries[position]
|
||||
query = dict(id=queue_id, company=company_id)
|
||||
updated = Queue.objects(entries__task=task_id, **query).update_one(
|
||||
pull__entries=entry, last_update=datetime.utcnow()
|
||||
)
|
||||
if not updated:
|
||||
raise errors.bad_request.RemovedDuringReposition(
|
||||
task=task_id, **query
|
||||
)
|
||||
inst = {"$push": {"entries": {"$each": [entry.to_proper_dict()]}}}
|
||||
if new_position >= 0:
|
||||
inst["$push"]["entries"]["$position"] = new_position
|
||||
res = Queue.objects(entries__task__ne=task_id, **query).update_one(
|
||||
__raw__=inst
|
||||
)
|
||||
if not res:
|
||||
raise errors.bad_request.FailedAddingDuringReposition(
|
||||
task=task_id, **query
|
||||
)
|
||||
without_entry = {
|
||||
"$filter": {
|
||||
"input": "$entries",
|
||||
"as": "entry",
|
||||
"cond": {"$ne": ["$$entry.task", task_id]},
|
||||
}
|
||||
}
|
||||
task_entry = {
|
||||
"$filter": {
|
||||
"input": "$entries",
|
||||
"as": "entry",
|
||||
"cond": {"$eq": ["$$entry.task", task_id]},
|
||||
}
|
||||
}
|
||||
if move_count == MOVE_FIRST:
|
||||
operations = [
|
||||
{
|
||||
"$set": {
|
||||
"entries": {"$concatArrays": [task_entry, without_entry]}
|
||||
}
|
||||
}
|
||||
]
|
||||
elif move_count == MOVE_LAST:
|
||||
operations = [
|
||||
{
|
||||
"$set": {
|
||||
"entries": {"$concatArrays": [without_entry, task_entry]}
|
||||
}
|
||||
}
|
||||
]
|
||||
else:
|
||||
operations = [
|
||||
{
|
||||
"$set": {
|
||||
"new_pos": {
|
||||
"$add": [
|
||||
{"$indexOfArray": ["$entries.task", task_id]},
|
||||
move_count,
|
||||
]
|
||||
},
|
||||
"without_entry": without_entry,
|
||||
"task_entry": task_entry,
|
||||
}
|
||||
},
|
||||
{
|
||||
"$set": {
|
||||
"entries": {
|
||||
"$switch": {
|
||||
"branches": [
|
||||
{
|
||||
"case": {"$lte": ["$new_pos", 0]},
|
||||
"then": {
|
||||
"$concatArrays": [
|
||||
"$task_entry",
|
||||
"$without_entry",
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"case": {
|
||||
"$gte": [
|
||||
"$new_pos",
|
||||
{"$size": "$without_entry"},
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"$concatArrays": [
|
||||
"$without_entry",
|
||||
"$task_entry",
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
"default": {
|
||||
"$concatArrays": [
|
||||
{"$slice": ["$without_entry", "$new_pos"]},
|
||||
"$task_entry",
|
||||
{
|
||||
"$slice": [
|
||||
"$without_entry",
|
||||
"$new_pos",
|
||||
{"$size": "$without_entry"},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{"$unset": ["new_pos", "without_entry", "task_entry"]},
|
||||
]
|
||||
|
||||
return new_position
|
||||
updated = Queue.objects(
|
||||
id=queue_id, company=company_id, entries__task=task_id
|
||||
).update_one(__raw__=operations)
|
||||
|
||||
if not updated:
|
||||
raise errors.bad_request.FailedAddingDuringReposition(task=task_id)
|
||||
|
||||
return get_queue_and_task_position()[1]
|
||||
|
||||
def count_entries(self, company: str, queue_id: str) -> Optional[int]:
|
||||
res = next(
|
||||
|
||||
@@ -80,7 +80,7 @@ class QueueMetrics:
|
||||
logged = 0
|
||||
for q in queues:
|
||||
queue_doc = make_doc(q)
|
||||
self.es.index(index=es_index, body=queue_doc)
|
||||
self.es.index(index=es_index, document=queue_doc)
|
||||
redis_key = _queue_metrics_key_pattern.format(queue=q.id)
|
||||
redis.set(redis_key, json.dumps(queue_doc))
|
||||
logged += 1
|
||||
|
||||
@@ -8,8 +8,7 @@ from typing import Sequence, Optional
|
||||
|
||||
import dpath
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
from apiserver.bll.query import Builder as QueryBuilder
|
||||
from apiserver.bll.util import get_server_uuid
|
||||
|
||||
48
apiserver/bll/storage/__init__.py
Normal file
48
apiserver/bll/storage/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from copy import copy
|
||||
|
||||
from boltons.cacheutils import cachedproperty
|
||||
from clearml.backend_config.bucket_config import (
|
||||
S3BucketConfigurations,
|
||||
AzureContainerConfigurations,
|
||||
GSBucketConfigurations,
|
||||
)
|
||||
|
||||
from apiserver.config_repo import config
|
||||
|
||||
|
||||
log = config.logger(__file__)
|
||||
|
||||
|
||||
class StorageBLL:
|
||||
default_aws_configs: S3BucketConfigurations = None
|
||||
conf = config.get("services.storage_credentials")
|
||||
|
||||
@cachedproperty
|
||||
def _default_aws_configs(self) -> S3BucketConfigurations:
|
||||
return S3BucketConfigurations.from_config(self.conf.get("aws.s3"))
|
||||
|
||||
@cachedproperty
|
||||
def _default_azure_configs(self) -> AzureContainerConfigurations:
|
||||
return AzureContainerConfigurations.from_config(self.conf.get("azure.storage"))
|
||||
|
||||
@cachedproperty
|
||||
def _default_gs_configs(self) -> GSBucketConfigurations:
|
||||
return GSBucketConfigurations.from_config(self.conf.get("google.storage"))
|
||||
|
||||
def get_azure_settings_for_company(
|
||||
self,
|
||||
company_id: str,
|
||||
) -> AzureContainerConfigurations:
|
||||
return copy(self._default_azure_configs)
|
||||
|
||||
def get_gs_settings_for_company(
|
||||
self,
|
||||
company_id: str,
|
||||
) -> GSBucketConfigurations:
|
||||
return copy(self._default_gs_configs)
|
||||
|
||||
def get_aws_settings_for_company(
|
||||
self,
|
||||
company_id: str,
|
||||
) -> S3BucketConfigurations:
|
||||
return copy(self._default_aws_configs)
|
||||
@@ -1,6 +1,5 @@
|
||||
from .task_bll import TaskBLL
|
||||
from .utils import (
|
||||
ChangeStatusRequest,
|
||||
update_project_time,
|
||||
validate_status_change,
|
||||
)
|
||||
|
||||
@@ -48,6 +48,7 @@ class Artifacts:
|
||||
def add_or_update_artifacts(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
artifacts: Sequence[ApiArtifact],
|
||||
force: bool,
|
||||
@@ -63,12 +64,13 @@ class Artifacts:
|
||||
f"set__execution__artifacts__{mongoengine_safe(name)}": value
|
||||
for name, value in artifacts.items()
|
||||
}
|
||||
return update_task(task, update_cmds=update_cmds)
|
||||
return update_task(task, user_id=user_id, update_cmds=update_cmds)
|
||||
|
||||
@classmethod
|
||||
def delete_artifacts(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
artifact_ids: Sequence[ArtifactId],
|
||||
force: bool,
|
||||
@@ -83,4 +85,4 @@ class Artifacts:
|
||||
f"unset__execution__artifacts__{id_}": 1 for id_ in set(artifact_ids)
|
||||
}
|
||||
|
||||
return update_task(task, update_cmds=delete_cmds)
|
||||
return update_task(task, user_id=user_id, update_cmds=delete_cmds)
|
||||
|
||||
@@ -63,6 +63,7 @@ class HyperParams:
|
||||
def delete_params(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
hyperparams: Sequence[HyperParamKey],
|
||||
force: bool,
|
||||
@@ -94,13 +95,17 @@ class HyperParams:
|
||||
delete_cmds[f"unset__hyperparams__{section}__{name}"] = 1
|
||||
|
||||
return update_task(
|
||||
task, update_cmds=delete_cmds, set_last_update=not properties_only
|
||||
task,
|
||||
user_id=user_id,
|
||||
update_cmds=delete_cmds,
|
||||
set_last_update=not properties_only,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def edit_params(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
hyperparams: Sequence[HyperParamItem],
|
||||
replace_hyperparams: str,
|
||||
@@ -129,7 +134,10 @@ class HyperParams:
|
||||
] = value
|
||||
|
||||
return update_task(
|
||||
task, update_cmds=update_cmds, set_last_update=not properties_only
|
||||
task,
|
||||
user_id=user_id,
|
||||
update_cmds=update_cmds,
|
||||
set_last_update=not properties_only,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -201,6 +209,7 @@ class HyperParams:
|
||||
def edit_configuration(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
configuration: Sequence[Configuration],
|
||||
replace_configuration: bool,
|
||||
@@ -219,11 +228,16 @@ class HyperParams:
|
||||
for name, value in configuration.items():
|
||||
update_cmds[f"set__configuration__{mongoengine_safe(name)}"] = value
|
||||
|
||||
return update_task(task, update_cmds=update_cmds)
|
||||
return update_task(task, user_id=user_id, update_cmds=update_cmds)
|
||||
|
||||
@classmethod
|
||||
def delete_configuration(
|
||||
cls, company_id: str, task_id: str, configuration: Sequence[str], force: bool
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
configuration: Sequence[str],
|
||||
force: bool,
|
||||
) -> int:
|
||||
task = get_task_for_update(company_id=company_id, task_id=task_id, force=force)
|
||||
|
||||
@@ -232,4 +246,4 @@ class HyperParams:
|
||||
for name in set(configuration)
|
||||
}
|
||||
|
||||
return update_task(task, update_cmds=delete_cmds)
|
||||
return update_task(task, user_id=user_id, update_cmds=delete_cmds)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import timedelta, datetime
|
||||
from time import sleep
|
||||
|
||||
from apiserver.bll.task import update_project_time
|
||||
from apiserver.bll.util import update_project_time
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model.task.task import TaskStatus, Task
|
||||
from apiserver.utilities.threads_manager import ThreadsManager
|
||||
@@ -85,6 +85,7 @@ class NonResponsiveTasksWatchdog:
|
||||
status_changed=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by="__apiserver__",
|
||||
)
|
||||
if updated:
|
||||
project_ids.add(task.project)
|
||||
|
||||
@@ -7,11 +7,12 @@ from redis import StrictRedis
|
||||
from six import string_types
|
||||
|
||||
import apiserver.database.utils as dbutils
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apierrors import errors, APIError
|
||||
from apiserver.apimodels.tasks import TaskInputModel
|
||||
from apiserver.bll.queue import QueueBLL
|
||||
from apiserver.bll.organization import OrgBLL, Tags
|
||||
from apiserver.bll.project import ProjectBLL
|
||||
from apiserver.bll.util import update_project_time
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.model import Model
|
||||
@@ -30,17 +31,20 @@ from apiserver.database.model.task.task import (
|
||||
TaskModelTypes,
|
||||
)
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.utils import get_company_or_none_constraint, id as create_id
|
||||
from apiserver.database.model.queue import Queue
|
||||
from apiserver.database.utils import (
|
||||
get_company_or_none_constraint,
|
||||
id as create_id,
|
||||
)
|
||||
from apiserver.es_factory import es_factory
|
||||
from apiserver.redis_manager import redman
|
||||
from apiserver.service_repo import APICall
|
||||
from apiserver.services.utils import validate_tags, escape_dict_field, escape_dict
|
||||
from .artifacts import artifacts_prepare_for_save
|
||||
from .param_utils import params_prepare_for_save
|
||||
from .utils import (
|
||||
ChangeStatusRequest,
|
||||
update_project_time,
|
||||
deleted_prefix,
|
||||
get_last_metric_updates,
|
||||
)
|
||||
|
||||
log = config.logger(__file__)
|
||||
@@ -77,7 +81,11 @@ class TaskBLL:
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
company_id, task_id, required_status=None, only_fields=None, allow_public=False,
|
||||
company_id,
|
||||
task_id,
|
||||
required_status=None,
|
||||
only_fields=None,
|
||||
allow_public=False,
|
||||
):
|
||||
if only_fields:
|
||||
if isinstance(only_fields, string_types):
|
||||
@@ -128,16 +136,16 @@ class TaskBLL:
|
||||
return list(q)
|
||||
|
||||
@staticmethod
|
||||
def create(call: APICall, fields: dict):
|
||||
identity = call.identity
|
||||
def create(company: str, user: str, fields: dict):
|
||||
now = datetime.utcnow()
|
||||
return Task(
|
||||
id=create_id(),
|
||||
user=identity.user,
|
||||
company=identity.company,
|
||||
user=user,
|
||||
company=company,
|
||||
created=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=user,
|
||||
**fields,
|
||||
)
|
||||
|
||||
@@ -257,6 +265,16 @@ class TaskBLL:
|
||||
not in [TaskSystemTags.development, EntityVisibility.archived.value]
|
||||
]
|
||||
|
||||
def ensure_int_labels(execution: dict) -> dict:
|
||||
if not execution:
|
||||
return execution
|
||||
|
||||
model_labels = execution.get("model_labels")
|
||||
if model_labels:
|
||||
execution["model_labels"] = {k: int(v) for k, v in model_labels.items()}
|
||||
|
||||
return execution
|
||||
|
||||
parent_task = (
|
||||
task.parent
|
||||
if task.parent and not task.parent.startswith(deleted_prefix)
|
||||
@@ -269,6 +287,7 @@ class TaskBLL:
|
||||
created=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=user_id,
|
||||
name=name or task.name,
|
||||
comment=comment or task.comment,
|
||||
parent=parent or parent_task,
|
||||
@@ -280,7 +299,7 @@ class TaskBLL:
|
||||
output=Output(destination=task.output.destination) if task.output else None,
|
||||
models=Models(input=input_models or task.models.input),
|
||||
container=escape_dict(container) or task.container,
|
||||
execution=execution_dict,
|
||||
execution=ensure_int_labels(execution_dict),
|
||||
configuration=params_dict.get("configuration") or task.configuration,
|
||||
hyperparams=params_dict.get("hyperparams") or task.hyperparams,
|
||||
)
|
||||
@@ -301,7 +320,7 @@ class TaskBLL:
|
||||
org_bll.update_tags(
|
||||
company_id,
|
||||
Tags.Task,
|
||||
project=new_task.project,
|
||||
projects=[new_task.project],
|
||||
tags=updated_tags,
|
||||
system_tags=updated_system_tags,
|
||||
)
|
||||
@@ -344,6 +363,7 @@ class TaskBLL:
|
||||
def set_last_update(
|
||||
task_ids: Collection[str],
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
last_update: datetime,
|
||||
**extra_updates,
|
||||
):
|
||||
@@ -364,6 +384,7 @@ class TaskBLL:
|
||||
upsert=False,
|
||||
last_update=last_update,
|
||||
last_change=last_update,
|
||||
last_changed_by=user_id,
|
||||
**updates,
|
||||
)
|
||||
return count
|
||||
@@ -372,6 +393,7 @@ class TaskBLL:
|
||||
def update_statistics(
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
last_update: datetime = None,
|
||||
last_iteration: int = None,
|
||||
last_iteration_max: int = None,
|
||||
@@ -400,50 +422,21 @@ class TaskBLL:
|
||||
elif last_iteration_max is not None:
|
||||
extra_updates.update(max__last_iteration=last_iteration_max)
|
||||
|
||||
raw_updates = {}
|
||||
if last_scalar_events is not None:
|
||||
max_values = config.get("services.tasks.max_last_metrics", 2000)
|
||||
total_metrics = set()
|
||||
if max_values:
|
||||
query = dict(id=task_id)
|
||||
to_add = sum(len(v) for m, v in last_scalar_events.items())
|
||||
if to_add <= max_values:
|
||||
query[f"unique_metrics__{max_values-to_add}__exists"] = True
|
||||
task = Task.objects(**query).only("unique_metrics").first()
|
||||
if task and task.unique_metrics:
|
||||
total_metrics = set(task.unique_metrics)
|
||||
|
||||
new_metrics = []
|
||||
for metric_key, metric_data in last_scalar_events.items():
|
||||
for variant_key, variant_data in metric_data.items():
|
||||
metric = (
|
||||
f"{variant_data.get('metric')}/{variant_data.get('variant')}"
|
||||
)
|
||||
if max_values:
|
||||
if (
|
||||
len(total_metrics) >= max_values
|
||||
and metric not in total_metrics
|
||||
):
|
||||
continue
|
||||
total_metrics.add(metric)
|
||||
|
||||
new_metrics.append(metric)
|
||||
path = f"last_metrics__{metric_key}__{variant_key}"
|
||||
for key, value in variant_data.items():
|
||||
if key == "min_value":
|
||||
extra_updates[f"min__{path}__min_value"] = value
|
||||
elif key == "max_value":
|
||||
extra_updates[f"max__{path}__max_value"] = value
|
||||
elif key in ("metric", "variant", "value"):
|
||||
extra_updates[f"set__{path}__{key}"] = value
|
||||
if new_metrics:
|
||||
extra_updates["add_to_set__unique_metrics"] = new_metrics
|
||||
get_last_metric_updates(
|
||||
task_id=task_id,
|
||||
last_scalar_events=last_scalar_events,
|
||||
raw_updates=raw_updates,
|
||||
extra_updates=extra_updates,
|
||||
)
|
||||
|
||||
if last_events is not None:
|
||||
|
||||
def events_per_type(metric_data: Dict[str, dict]) -> Dict[str, EventStats]:
|
||||
def events_per_type(metric_data_: Dict[str, dict]) -> Dict[str, EventStats]:
|
||||
return {
|
||||
event_type: EventStats(last_update=event["timestamp"])
|
||||
for event_type, event in metric_data.items()
|
||||
for event_type, event in metric_data_.items()
|
||||
}
|
||||
|
||||
metric_stats = {
|
||||
@@ -454,28 +447,54 @@ class TaskBLL:
|
||||
}
|
||||
extra_updates["metric_stats"] = metric_stats
|
||||
|
||||
return TaskBLL.set_last_update(
|
||||
ret = TaskBLL.set_last_update(
|
||||
task_ids=[task_id],
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
last_update=last_update,
|
||||
**extra_updates,
|
||||
)
|
||||
if ret and raw_updates:
|
||||
Task.objects(id=task_id).update_one(__raw__=[{"$set": raw_updates}])
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def remove_task_from_all_queues(company_id: str, task_id: str) -> int:
|
||||
return Queue.objects(company=company_id, entries__task=task_id).update(
|
||||
pull__entries__task=task_id, last_update=datetime.utcnow()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def dequeue_and_change_status(
|
||||
cls, task: Task, company_id: str, status_message: str, status_reason: str,
|
||||
cls,
|
||||
task: Task,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
remove_from_all_queues=False,
|
||||
new_status=None,
|
||||
):
|
||||
try:
|
||||
cls.dequeue(task, company_id)
|
||||
except errors.bad_request.InvalidQueueOrTaskNotQueued:
|
||||
cls.dequeue(task, company_id, silent_fail=True)
|
||||
except APIError:
|
||||
# dequeue may fail if the queue was deleted
|
||||
pass
|
||||
|
||||
if remove_from_all_queues:
|
||||
cls.remove_task_from_all_queues(company_id=company_id, task_id=task.id)
|
||||
|
||||
if task.status not in [TaskStatus.queued, TaskStatus.in_progress]:
|
||||
return {"updated": 0}
|
||||
|
||||
return ChangeStatusRequest(
|
||||
task=task,
|
||||
new_status=task.enqueue_status or TaskStatus.created,
|
||||
new_status=new_status or task.enqueue_status or TaskStatus.created,
|
||||
status_reason=status_reason,
|
||||
status_message=status_message,
|
||||
user_id=user_id,
|
||||
force=True,
|
||||
).execute(enqueue_status=None)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
from typing import Sequence, Set, Tuple
|
||||
from typing import Sequence, Set, Tuple, Union
|
||||
|
||||
import attr
|
||||
from boltons.iterutils import partition, bucketize, first
|
||||
from boltons.iterutils import partition, bucketize, first, chunked_iter
|
||||
from furl import furl
|
||||
from mongoengine import NotUniqueError
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
|
||||
@@ -25,7 +26,6 @@ from apiserver.database.utils import id as db_id
|
||||
|
||||
log = config.logger(__file__)
|
||||
event_bll = EventBLL()
|
||||
async_events_delete = config.get("services.tasks.async_events_delete", False)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@@ -68,49 +68,74 @@ class CleanupResult:
|
||||
)
|
||||
|
||||
|
||||
def collect_plot_image_urls(company: str, task_or_model: str) -> Set[str]:
|
||||
def collect_plot_image_urls(
|
||||
company: str, task_or_model: Union[str, Sequence[str]]
|
||||
) -> Set[str]:
|
||||
urls = set()
|
||||
next_scroll_id = None
|
||||
while True:
|
||||
events, next_scroll_id = event_bll.get_plot_image_urls(
|
||||
company_id=company, task_id=task_or_model, scroll_id=next_scroll_id
|
||||
)
|
||||
if not events:
|
||||
break
|
||||
for event in events:
|
||||
event_urls = event.get(PlotFields.source_urls)
|
||||
if event_urls:
|
||||
urls.update(set(event_urls))
|
||||
task_ids = task_or_model if isinstance(task_or_model, list) else [task_or_model]
|
||||
for tasks in chunked_iter(task_ids, 100):
|
||||
next_scroll_id = None
|
||||
while True:
|
||||
events, next_scroll_id = event_bll.get_plot_image_urls(
|
||||
company_id=company, task_ids=tasks, scroll_id=next_scroll_id
|
||||
)
|
||||
if not events:
|
||||
break
|
||||
for event in events:
|
||||
event_urls = event.get(PlotFields.source_urls)
|
||||
if event_urls:
|
||||
urls.update(set(event_urls))
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
def collect_debug_image_urls(company: str, task_or_model: str) -> Set[str]:
|
||||
def collect_debug_image_urls(
|
||||
company: str, task_or_model: Union[str, Sequence[str]]
|
||||
) -> Set[str]:
|
||||
"""
|
||||
Return the set of unique image urls
|
||||
Uses DebugImagesIterator to make sure that we do not retrieve recycled urls
|
||||
"""
|
||||
after_key = None
|
||||
urls = set()
|
||||
while True:
|
||||
res, after_key = event_bll.get_debug_image_urls(
|
||||
company_id=company, task_id=task_or_model, after_key=after_key,
|
||||
)
|
||||
urls.update(res)
|
||||
if not after_key:
|
||||
break
|
||||
task_ids = task_or_model if isinstance(task_or_model, list) else [task_or_model]
|
||||
for tasks in chunked_iter(task_ids, 100):
|
||||
after_key = None
|
||||
while True:
|
||||
res, after_key = event_bll.get_debug_image_urls(
|
||||
company_id=company,
|
||||
task_ids=tasks,
|
||||
after_key=after_key,
|
||||
)
|
||||
urls.update(res)
|
||||
if not after_key:
|
||||
break
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
supported_storage_types = {
|
||||
"https://": StorageType.fileserver,
|
||||
"http://": StorageType.fileserver,
|
||||
"s3://": StorageType.s3,
|
||||
"azure://": StorageType.azure,
|
||||
"gs://": StorageType.gs,
|
||||
}
|
||||
|
||||
supported_storage_types.update(
|
||||
{
|
||||
p: StorageType.fileserver
|
||||
for p in config.get(
|
||||
"services.async_urls_delete.fileserver.url_prefixes",
|
||||
["https://", "http://"],
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _schedule_for_delete(
|
||||
company: str, user: str, task_id: str, urls: Set[str], can_delete_folders: bool,
|
||||
company: str,
|
||||
user: str,
|
||||
task_id: str,
|
||||
urls: Set[str],
|
||||
can_delete_folders: bool,
|
||||
) -> Set[str]:
|
||||
urls_per_storage = bucketize(
|
||||
urls,
|
||||
@@ -129,7 +154,14 @@ def _schedule_for_delete(
|
||||
for url in storage_urls:
|
||||
folder = None
|
||||
if delete_folders:
|
||||
folder, _, _ = url.rpartition("/")
|
||||
try:
|
||||
parsed = furl(url)
|
||||
if parsed.path and len(parsed.path.segments) > 1:
|
||||
folder = parsed.remove(
|
||||
args=True, fragment=True, path=parsed.path.segments[-1]
|
||||
).url.rstrip("/")
|
||||
except Exception as ex:
|
||||
pass
|
||||
|
||||
to_delete = folder or url
|
||||
if to_delete in scheduled_to_delete:
|
||||
@@ -185,7 +217,7 @@ def cleanup_task(
|
||||
task, force
|
||||
)
|
||||
delete_external_artifacts = delete_external_artifacts and config.get(
|
||||
"services.async_urls_delete.enabled", False
|
||||
"services.async_urls_delete.enabled", True
|
||||
)
|
||||
event_urls, artifact_urls, model_urls = set(), set(), set()
|
||||
if return_file_urls or delete_external_artifacts:
|
||||
@@ -203,8 +235,13 @@ def cleanup_task(
|
||||
|
||||
deleted_task_id = f"{deleted_prefix}{task.id}"
|
||||
updated_children = 0
|
||||
now = datetime.utcnow()
|
||||
if update_children:
|
||||
updated_children = Task.objects(parent=task.id).update(parent=deleted_task_id)
|
||||
updated_children = Task.objects(parent=task.id).update(
|
||||
parent=deleted_task_id,
|
||||
last_change=now,
|
||||
last_changed_by=user,
|
||||
)
|
||||
|
||||
deleted_models = 0
|
||||
updated_models = 0
|
||||
@@ -212,37 +249,41 @@ def cleanup_task(
|
||||
if not models:
|
||||
continue
|
||||
if delete_output_models and allow_delete:
|
||||
model_ids = set(m.id for m in models if m.id not in in_use_model_ids)
|
||||
for m_id in model_ids:
|
||||
model_ids = list({m.id for m in models if m.id not in in_use_model_ids})
|
||||
if model_ids:
|
||||
if return_file_urls or delete_external_artifacts:
|
||||
event_urls.update(collect_debug_image_urls(task.company, m_id))
|
||||
event_urls.update(collect_plot_image_urls(task.company, m_id))
|
||||
try:
|
||||
event_bll.delete_task_events(
|
||||
task.company,
|
||||
m_id,
|
||||
allow_locked=True,
|
||||
model=True,
|
||||
async_delete=async_events_delete,
|
||||
)
|
||||
except errors.bad_request.InvalidModelId as ex:
|
||||
log.info(f"Error deleting events for the model {m_id}: {str(ex)}")
|
||||
event_urls.update(collect_debug_image_urls(task.company, model_ids))
|
||||
event_urls.update(collect_plot_image_urls(task.company, model_ids))
|
||||
|
||||
event_bll.delete_multi_task_events(
|
||||
task.company,
|
||||
model_ids,
|
||||
model=True,
|
||||
)
|
||||
deleted_models += Model.objects(id__in=model_ids).delete()
|
||||
|
||||
deleted_models += Model.objects(id__in=list(model_ids)).delete()
|
||||
if in_use_model_ids:
|
||||
Model.objects(id__in=list(in_use_model_ids)).update(unset__task=1)
|
||||
Model.objects(id__in=list(in_use_model_ids)).update(
|
||||
unset__task=1,
|
||||
set__last_change=now,
|
||||
set__last_changed_by=user,
|
||||
)
|
||||
continue
|
||||
|
||||
if update_children:
|
||||
updated_models += Model.objects(id__in=[m.id for m in models]).update(
|
||||
task=deleted_task_id
|
||||
task=deleted_task_id,
|
||||
last_change=now,
|
||||
last_changed_by=user,
|
||||
)
|
||||
else:
|
||||
Model.objects(id__in=[m.id for m in models]).update(unset__task=1)
|
||||
Model.objects(id__in=[m.id for m in models]).update(
|
||||
unset__task=1,
|
||||
set__last_change=now,
|
||||
set__last_changed_by=user,
|
||||
)
|
||||
|
||||
event_bll.delete_task_events(
|
||||
task.company, task.id, allow_locked=force, async_delete=async_events_delete
|
||||
)
|
||||
event_bll.delete_task_events(task.company, task.id, allow_locked=force)
|
||||
|
||||
if delete_external_artifacts:
|
||||
scheduled = _schedule_for_delete(
|
||||
@@ -285,7 +326,8 @@ def verify_task_children_and_ouptuts(
|
||||
|
||||
model_fields = ["id", "ready", "uri"]
|
||||
published_models, draft_models = partition(
|
||||
Model.objects(task=task.id).only(*model_fields), key=attrgetter("ready"),
|
||||
Model.objects(task=task.id).only(*model_fields),
|
||||
key=attrgetter("ready"),
|
||||
)
|
||||
if not force and published_models:
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
|
||||
@@ -7,9 +7,9 @@ from apiserver.bll.task import (
|
||||
TaskBLL,
|
||||
validate_status_change,
|
||||
ChangeStatusRequest,
|
||||
update_project_time,
|
||||
)
|
||||
from apiserver.bll.task.task_cleanup import cleanup_task, CleanupResult
|
||||
from apiserver.bll.util import update_project_time
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.model import Model
|
||||
@@ -23,6 +23,7 @@ from apiserver.database.model.task.task import (
|
||||
Execution,
|
||||
DEFAULT_LAST_ITERATION,
|
||||
)
|
||||
from apiserver.database.utils import get_options
|
||||
from apiserver.utilities.dicts import nested_set
|
||||
|
||||
log = config.logger(__file__)
|
||||
@@ -30,7 +31,11 @@ queue_bll = QueueBLL()
|
||||
|
||||
|
||||
def archive_task(
|
||||
task: Union[str, Task], company_id: str, status_message: str, status_reason: str,
|
||||
task: Union[str, Task],
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
) -> int:
|
||||
"""
|
||||
Deque and archive task
|
||||
@@ -42,6 +47,7 @@ def archive_task(
|
||||
company_id=company_id,
|
||||
only=(
|
||||
"id",
|
||||
"company",
|
||||
"execution",
|
||||
"status",
|
||||
"project",
|
||||
@@ -52,7 +58,12 @@ def archive_task(
|
||||
)
|
||||
try:
|
||||
TaskBLL.dequeue_and_change_status(
|
||||
task, company_id, status_message, status_reason,
|
||||
task,
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
status_message=status_message,
|
||||
status_reason=status_reason,
|
||||
remove_from_all_queues=True,
|
||||
)
|
||||
except APIError:
|
||||
# dequeue may fail if the task was not enqueued
|
||||
@@ -63,36 +74,73 @@ def archive_task(
|
||||
status_reason=status_reason,
|
||||
add_to_set__system_tags=EntityVisibility.archived.value,
|
||||
last_change=datetime.utcnow(),
|
||||
last_changed_by=user_id,
|
||||
)
|
||||
|
||||
|
||||
def unarchive_task(
|
||||
task: str, company_id: str, status_message: str, status_reason: str,
|
||||
task: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
) -> int:
|
||||
"""
|
||||
Unarchive task. Return 1 if successful
|
||||
"""
|
||||
task = TaskBLL.get_task_with_access(
|
||||
task, company_id=company_id, only=("id",), requires_write_access=True,
|
||||
task,
|
||||
company_id=company_id,
|
||||
only=("id",),
|
||||
requires_write_access=True,
|
||||
)
|
||||
return task.update(
|
||||
status_message=status_message,
|
||||
status_reason=status_reason,
|
||||
pull__system_tags=EntityVisibility.archived.value,
|
||||
last_change=datetime.utcnow(),
|
||||
last_changed_by=user_id,
|
||||
)
|
||||
|
||||
|
||||
def dequeue_task(
|
||||
task_id: str, company_id: str, status_message: str, status_reason: str,
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
remove_from_all_queues: bool = False,
|
||||
new_status=None,
|
||||
) -> Tuple[int, dict]:
|
||||
query = dict(id=task_id, company=company_id)
|
||||
task = Task.get_for_writing(**query)
|
||||
if new_status and new_status not in get_options(TaskStatus):
|
||||
raise errors.bad_request.ValidationError(f"Invalid task status: {new_status}")
|
||||
|
||||
# get the task without write access to make sure that it actually exists
|
||||
task = Task.get(
|
||||
id=task_id,
|
||||
company=company_id,
|
||||
_only=(
|
||||
"id",
|
||||
"company",
|
||||
"execution",
|
||||
"status",
|
||||
"project",
|
||||
"enqueue_status",
|
||||
),
|
||||
include_public=True,
|
||||
)
|
||||
if not task:
|
||||
raise errors.bad_request.InvalidTaskId(**query)
|
||||
TaskBLL.remove_task_from_all_queues(company_id, task_id=task_id)
|
||||
return 1, {"updated": 0}
|
||||
|
||||
res = TaskBLL.dequeue_and_change_status(
|
||||
task, company_id, status_message=status_message, status_reason=status_reason,
|
||||
task,
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
status_message=status_message,
|
||||
status_reason=status_reason,
|
||||
remove_from_all_queues=remove_from_all_queues,
|
||||
new_status=new_status,
|
||||
)
|
||||
return 1, res
|
||||
|
||||
@@ -100,6 +148,7 @@ def dequeue_task(
|
||||
def enqueue_task(
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
queue_id: str,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
@@ -139,6 +188,7 @@ def enqueue_task(
|
||||
status_message=status_message,
|
||||
allow_same_state_transition=False,
|
||||
force=force,
|
||||
user_id=user_id,
|
||||
).execute(enqueue_status=task.status)
|
||||
|
||||
try:
|
||||
@@ -151,6 +201,7 @@ def enqueue_task(
|
||||
new_status=task.status,
|
||||
force=True,
|
||||
status_reason="failed enqueueing",
|
||||
user_id=user_id,
|
||||
).execute(enqueue_status=None)
|
||||
raise
|
||||
|
||||
@@ -220,8 +271,10 @@ def delete_task(
|
||||
TaskBLL.dequeue_and_change_status(
|
||||
task,
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
status_message=status_message,
|
||||
status_reason=status_reason,
|
||||
remove_from_all_queues=True,
|
||||
)
|
||||
except APIError:
|
||||
# dequeue may fail if the task was not enqueued
|
||||
@@ -240,6 +293,7 @@ def delete_task(
|
||||
if move_to_trash:
|
||||
# make sure that whatever changes were done to the task are saved
|
||||
# the task itself will be deleted later in the move_tasks_to_trash operation
|
||||
task.last_update = datetime.utcnow()
|
||||
task.save()
|
||||
else:
|
||||
task.delete()
|
||||
@@ -274,6 +328,8 @@ def reset_task(
|
||||
# dequeue may fail if the task was not enqueued
|
||||
pass
|
||||
|
||||
TaskBLL.remove_task_from_all_queues(company_id=company_id, task_id=task.id)
|
||||
|
||||
cleaned_up = cleanup_task(
|
||||
company=company_id,
|
||||
user=user_id,
|
||||
@@ -296,11 +352,17 @@ def reset_task(
|
||||
unset__output__error=1,
|
||||
unset__last_worker=1,
|
||||
unset__last_worker_report=1,
|
||||
unset__started=1,
|
||||
unset__completed=1,
|
||||
unset__published=1,
|
||||
unset__active_duration=1,
|
||||
unset__enqueue_status=1,
|
||||
)
|
||||
|
||||
if clear_all:
|
||||
updates.update(
|
||||
set__execution=Execution(), unset__script=1,
|
||||
set__execution=Execution(),
|
||||
unset__script=1,
|
||||
)
|
||||
else:
|
||||
updates.update(unset__execution__queue=1)
|
||||
@@ -319,12 +381,8 @@ def reset_task(
|
||||
force=force,
|
||||
status_reason="reset",
|
||||
status_message="reset",
|
||||
user_id=user_id,
|
||||
).execute(
|
||||
started=None,
|
||||
completed=None,
|
||||
published=None,
|
||||
active_duration=None,
|
||||
enqueue_status=None,
|
||||
**updates,
|
||||
)
|
||||
|
||||
@@ -334,8 +392,9 @@ def reset_task(
|
||||
def publish_task(
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
force: bool,
|
||||
publish_model_func: Callable[[str, str], Any] = None,
|
||||
publish_model_func: Callable[[str, str, str], Any] = None,
|
||||
status_message: str = "",
|
||||
status_reason: str = "",
|
||||
) -> dict:
|
||||
@@ -363,7 +422,7 @@ def publish_task(
|
||||
.first()
|
||||
)
|
||||
if model and not model.ready:
|
||||
publish_model_func(model.id, company_id)
|
||||
publish_model_func(model.id, company_id, user_id)
|
||||
|
||||
# set task status to published, and update (or set) it's new output (view and models)
|
||||
return ChangeStatusRequest(
|
||||
@@ -372,6 +431,7 @@ def publish_task(
|
||||
force=force,
|
||||
status_reason=status_reason,
|
||||
status_message=status_message,
|
||||
user_id=user_id,
|
||||
).execute(published=datetime.utcnow(), output=output)
|
||||
|
||||
except Exception as ex:
|
||||
@@ -384,7 +444,12 @@ def publish_task(
|
||||
|
||||
|
||||
def stop_task(
|
||||
task_id: str, company_id: str, user_name: str, status_reason: str, force: bool,
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
user_name: str,
|
||||
status_reason: str,
|
||||
force: bool,
|
||||
) -> dict:
|
||||
"""
|
||||
Stop a running task. Requires task status 'in_progress' and
|
||||
@@ -446,4 +511,5 @@ def stop_task(
|
||||
status_reason=status_reason,
|
||||
status_message=status_message,
|
||||
force=force,
|
||||
user_id=user_id,
|
||||
).execute()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from datetime import datetime
|
||||
from typing import Sequence, Union
|
||||
|
||||
import attr
|
||||
import six
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.bll.util import update_project_time
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.task.task import Task, TaskStatus, TaskSystemTags
|
||||
from apiserver.database.utils import get_options
|
||||
from apiserver.utilities.attrs import typed_attrs
|
||||
@@ -26,6 +27,7 @@ class ChangeStatusRequest(object):
|
||||
force = attr.ib(type=bool, default=False)
|
||||
allow_same_state_transition = attr.ib(type=bool, default=True)
|
||||
current_status_override = attr.ib(default=None)
|
||||
user_id = attr.ib(type=str, default=None)
|
||||
|
||||
def execute(self, **kwargs):
|
||||
current_status = self.current_status_override or self.task.status
|
||||
@@ -44,6 +46,7 @@ class ChangeStatusRequest(object):
|
||||
status_changed=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=self.user_id,
|
||||
)
|
||||
|
||||
if self.new_status == TaskStatus.queued:
|
||||
@@ -154,16 +157,6 @@ def get_possible_status_changes(current_status):
|
||||
return possible
|
||||
|
||||
|
||||
def update_project_time(project_ids: Union[str, Sequence[str]]):
|
||||
if not project_ids:
|
||||
return
|
||||
|
||||
if isinstance(project_ids, str):
|
||||
project_ids = [project_ids]
|
||||
|
||||
return Project.objects(id__in=project_ids).update(last_update=datetime.utcnow())
|
||||
|
||||
|
||||
def get_task_for_update(
|
||||
company_id: str, task_id: str, allow_all_statuses: bool = False, force: bool = False
|
||||
) -> Task:
|
||||
@@ -187,9 +180,88 @@ def get_task_for_update(
|
||||
return task
|
||||
|
||||
|
||||
def update_task(task: Task, update_cmds: dict, set_last_update: bool = True):
|
||||
def update_task(
|
||||
task: Task, user_id: str, update_cmds: dict, set_last_update: bool = True
|
||||
):
|
||||
now = datetime.utcnow()
|
||||
last_updates = dict(last_change=now)
|
||||
last_updates = dict(last_change=now, last_changed_by=user_id)
|
||||
if set_last_update:
|
||||
last_updates.update(last_update=now)
|
||||
return task.update(**update_cmds, **last_updates)
|
||||
|
||||
|
||||
def get_last_metric_updates(
|
||||
task_id: str,
|
||||
last_scalar_events: dict,
|
||||
raw_updates: dict,
|
||||
extra_updates: dict,
|
||||
model_events: bool = False,
|
||||
):
|
||||
max_values = config.get("services.tasks.max_last_metrics", 2000)
|
||||
total_metrics = set()
|
||||
if max_values:
|
||||
query = dict(id=task_id)
|
||||
to_add = sum(len(v) for m, v in last_scalar_events.items())
|
||||
if to_add <= max_values:
|
||||
query[f"unique_metrics__{max_values - to_add}__exists"] = True
|
||||
db_cls = Model if model_events else Task
|
||||
task = db_cls.objects(**query).only("unique_metrics").first()
|
||||
if task and task.unique_metrics:
|
||||
total_metrics = set(task.unique_metrics)
|
||||
|
||||
new_metrics = []
|
||||
|
||||
def add_last_metric_conditional_update(
|
||||
metric_path: str, metric_value, iter_value: int, is_min: bool
|
||||
):
|
||||
"""
|
||||
Build an aggregation for an atomic update of the min or max value and the corresponding iteration
|
||||
"""
|
||||
if is_min:
|
||||
field_prefix = "min"
|
||||
op = "$gt"
|
||||
else:
|
||||
field_prefix = "max"
|
||||
op = "$lt"
|
||||
|
||||
value_field = f"{metric_path}__{field_prefix}_value".replace("__", ".")
|
||||
condition = {
|
||||
"$or": [
|
||||
{"$lte": [f"${value_field}", None]},
|
||||
{op: [f"${value_field}", metric_value]},
|
||||
]
|
||||
}
|
||||
raw_updates[value_field] = {
|
||||
"$cond": [condition, metric_value, f"${value_field}"]
|
||||
}
|
||||
|
||||
value_iteration_field = f"{metric_path}__{field_prefix}_value_iteration".replace(
|
||||
"__", "."
|
||||
)
|
||||
raw_updates[value_iteration_field] = {
|
||||
"$cond": [condition, iter_value, f"${value_iteration_field}"]
|
||||
}
|
||||
|
||||
for metric_key, metric_data in last_scalar_events.items():
|
||||
for variant_key, variant_data in metric_data.items():
|
||||
metric = f"{variant_data.get('metric')}/{variant_data.get('variant')}"
|
||||
if max_values:
|
||||
if len(total_metrics) >= max_values and metric not in total_metrics:
|
||||
continue
|
||||
total_metrics.add(metric)
|
||||
|
||||
new_metrics.append(metric)
|
||||
path = f"last_metrics__{metric_key}__{variant_key}"
|
||||
for key, value in variant_data.items():
|
||||
if key in ("min_value", "max_value"):
|
||||
add_last_metric_conditional_update(
|
||||
metric_path=path,
|
||||
metric_value=value,
|
||||
iter_value=variant_data.get(f"{key}_iter", 0),
|
||||
is_min=(key == "min_value"),
|
||||
)
|
||||
elif key in ("metric", "variant", "value"):
|
||||
extra_updates[f"set__{path}__{key}"] = value
|
||||
|
||||
if new_metrics:
|
||||
extra_updates["add_to_set__unique_metrics"] = new_metrics
|
||||
|
||||
@@ -1,76 +1,24 @@
|
||||
import functools
|
||||
import itertools
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
Dict,
|
||||
Any,
|
||||
Set,
|
||||
Iterable,
|
||||
Tuple,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from boltons import iterutils
|
||||
|
||||
from apiserver.apierrors import APIError
|
||||
from apiserver.database.model import AttributedDocument
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.settings import Settings
|
||||
|
||||
|
||||
class SetFieldsResolver:
|
||||
"""
|
||||
The class receives set fields dictionary
|
||||
and for the set fields that require 'min' or 'max'
|
||||
operation replace them with a simple set in case the
|
||||
DB document does not have these fields set
|
||||
"""
|
||||
|
||||
SET_MODIFIERS = ("min", "max")
|
||||
|
||||
def __init__(self, set_fields: Dict[str, Any]):
|
||||
self.orig_fields = {}
|
||||
self.fields = {}
|
||||
self.add_fields(**set_fields)
|
||||
|
||||
def add_fields(self, **set_fields: Any):
|
||||
self.orig_fields.update(set_fields)
|
||||
self.fields.update(
|
||||
{
|
||||
f: fname
|
||||
for f, modifier, dunder, fname in (
|
||||
(f,) + f.partition("__") for f in set_fields.keys()
|
||||
)
|
||||
if dunder and modifier in self.SET_MODIFIERS
|
||||
}
|
||||
)
|
||||
|
||||
def _get_updated_name(self, doc: AttributedDocument, name: str) -> str:
|
||||
if name in self.fields and doc.get_field_value(self.fields[name]) is None:
|
||||
return self.fields[name]
|
||||
return name
|
||||
|
||||
def get_fields(self, doc: AttributedDocument):
|
||||
"""
|
||||
For the given document return the set fields instructions
|
||||
with min/max operations replaced with a single set in case
|
||||
the document does not have the field set
|
||||
"""
|
||||
return {
|
||||
self._get_updated_name(doc, name): value
|
||||
for name, value in self.orig_fields.items()
|
||||
}
|
||||
|
||||
def get_names(self) -> Set[str]:
|
||||
"""
|
||||
Returns the names of the fields that had min/max modifiers
|
||||
in the format suitable for projection (dot separated)
|
||||
"""
|
||||
return set(name.replace("__", ".") for name in self.fields.values())
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_server_uuid() -> Optional[str]:
|
||||
return Settings.get_by_key("server.uuid")
|
||||
@@ -132,3 +80,13 @@ def run_batch_operation(
|
||||
}
|
||||
)
|
||||
return results, failures
|
||||
|
||||
|
||||
def update_project_time(project_ids: Union[str, Sequence[str]]):
|
||||
if not project_ids:
|
||||
return
|
||||
|
||||
if isinstance(project_ids, str):
|
||||
project_ids = [project_ids]
|
||||
|
||||
return Project.objects(id__in=project_ids).update(last_update=datetime.utcnow())
|
||||
|
||||
@@ -5,13 +5,13 @@ from typing import Sequence, Set, Optional
|
||||
|
||||
import attr
|
||||
import elasticsearch.helpers
|
||||
from boltons.iterutils import partition
|
||||
from boltons.iterutils import partition, chunked_iter
|
||||
from pyhocon import ConfigTree
|
||||
|
||||
from apiserver.es_factory import es_factory
|
||||
from apiserver.apierrors import APIError
|
||||
from apiserver.apierrors.errors import bad_request, server_error
|
||||
from apiserver.apimodels.workers import (
|
||||
DEFAULT_TIMEOUT,
|
||||
IdNameEntry,
|
||||
WorkerEntry,
|
||||
StatusReportRequest,
|
||||
@@ -30,12 +30,14 @@ from apiserver.redis_manager import redman
|
||||
from apiserver.tools import safe_get
|
||||
from .stats import WorkerStats
|
||||
|
||||
|
||||
log = config.logger(__file__)
|
||||
|
||||
|
||||
class WorkerBLL:
|
||||
def __init__(self, es=None, redis=None):
|
||||
self.es_client = es or es_factory.connect("workers")
|
||||
self.config = config.get("services.workers", ConfigTree())
|
||||
self.redis = redis or redman.connection("workers")
|
||||
self._stats = WorkerStats(self.es_client)
|
||||
|
||||
@@ -68,7 +70,7 @@ class WorkerBLL:
|
||||
"""
|
||||
key = WorkerBLL._get_worker_key(company_id, user_id, worker)
|
||||
|
||||
timeout = timeout or DEFAULT_TIMEOUT
|
||||
timeout = timeout or int(self.config.get("default_worker_timeout_sec", 10 * 60))
|
||||
queues = queues or []
|
||||
|
||||
with translate_errors_context():
|
||||
@@ -141,8 +143,6 @@ class WorkerBLL:
|
||||
|
||||
try:
|
||||
entry.ip = ip
|
||||
now = datetime.utcnow()
|
||||
entry.last_activity_time = now
|
||||
|
||||
if tags is not None:
|
||||
entry.tags = tags
|
||||
@@ -150,15 +150,16 @@ class WorkerBLL:
|
||||
entry.system_tags = system_tags
|
||||
|
||||
if report.machine_stats:
|
||||
self._log_stats_to_es(
|
||||
self.log_stats_to_es(
|
||||
company_id=company_id,
|
||||
company_name=entry.company.name,
|
||||
worker=entry.key,
|
||||
worker_id=report.worker,
|
||||
timestamp=report.timestamp,
|
||||
task=report.task,
|
||||
machine_stats=report.machine_stats,
|
||||
)
|
||||
|
||||
now = datetime.utcnow()
|
||||
entry.last_activity_time = now
|
||||
entry.queue = report.queue
|
||||
|
||||
if report.queues:
|
||||
@@ -175,6 +176,7 @@ class WorkerBLL:
|
||||
last_worker_report=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=user_id,
|
||||
)
|
||||
# modify(new=True, ...) returns the modified object
|
||||
task = Task.objects(**query).modify(new=True, **update)
|
||||
@@ -200,6 +202,24 @@ class WorkerBLL:
|
||||
finally:
|
||||
self._save_worker(entry)
|
||||
|
||||
def get_count(
|
||||
self,
|
||||
company_id: str,
|
||||
last_seen: Optional[int] = None,
|
||||
tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
):
|
||||
if not last_seen:
|
||||
return len(
|
||||
self._get_keys(company_id, user_tags=tags, system_tags=system_tags)
|
||||
)
|
||||
|
||||
return len(
|
||||
self.get_all(
|
||||
company_id, last_seen=last_seen, tags=tags, system_tags=system_tags
|
||||
)
|
||||
)
|
||||
|
||||
def get_all(
|
||||
self,
|
||||
company_id: str,
|
||||
@@ -235,18 +255,15 @@ class WorkerBLL:
|
||||
tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> Sequence[WorkerResponseEntry]:
|
||||
|
||||
helpers = list(
|
||||
map(
|
||||
WorkerConversionHelper.from_worker_entry,
|
||||
self.get_all(
|
||||
company_id=company_id,
|
||||
last_seen=last_seen,
|
||||
tags=tags,
|
||||
system_tags=system_tags,
|
||||
),
|
||||
helpers = [
|
||||
WorkerConversionHelper.from_worker_entry(entry)
|
||||
for entry in self.get_all(
|
||||
company_id=company_id,
|
||||
last_seen=last_seen,
|
||||
tags=tags,
|
||||
system_tags=system_tags,
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
task_ids = set(filter(None, (helper.task_id for helper in helpers)))
|
||||
all_queues = set(
|
||||
@@ -265,9 +282,7 @@ class WorkerBLL:
|
||||
}
|
||||
},
|
||||
]
|
||||
queues_info = {
|
||||
res["_id"]: res for res in Queue.objects.aggregate(projection)
|
||||
}
|
||||
queues_info = {res["_id"]: res for res in Queue.aggregate(projection)}
|
||||
task_ids = task_ids.union(
|
||||
filter(
|
||||
None,
|
||||
@@ -396,15 +411,16 @@ class WorkerBLL:
|
||||
msg = "Failed saving worker entry"
|
||||
log.exception(msg)
|
||||
|
||||
def _get(
|
||||
def _get_keys(
|
||||
self,
|
||||
company: str,
|
||||
user: str = "*",
|
||||
worker_id: str = "*",
|
||||
user_tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> Sequence[WorkerEntry]:
|
||||
"""Get worker entries matching the company and user, worker patterns"""
|
||||
) -> Sequence[bytes]:
|
||||
if not (user_tags or system_tags):
|
||||
match = self._get_worker_key(company, user, "*")
|
||||
return list(self.redis.scan_iter(match))
|
||||
|
||||
def filter_by_user(in_keys: Set[bytes]) -> Set[bytes]:
|
||||
if user == "*":
|
||||
@@ -412,67 +428,79 @@ class WorkerBLL:
|
||||
user_bytes = user.encode()
|
||||
return {k for k in in_keys if user_bytes in k}
|
||||
|
||||
if user_tags or system_tags:
|
||||
worker_keys = set()
|
||||
for tags, tags_field in (
|
||||
(user_tags, "tags"),
|
||||
(system_tags, "systemtags"),
|
||||
):
|
||||
if not tags:
|
||||
continue
|
||||
timestamp = int(time())
|
||||
include, exclude = partition(tags, key=lambda x: x[0] != "-")
|
||||
if include:
|
||||
tagged_workers = set()
|
||||
for tag in include:
|
||||
tagged_workers_key = self._get_tagged_workers_key(
|
||||
company, tags_field, tag
|
||||
)
|
||||
self.redis.zremrangebyscore(
|
||||
tagged_workers_key, min=0, max=timestamp
|
||||
)
|
||||
tagged_workers.update(
|
||||
self.redis.zrange(tagged_workers_key, 0, -1)
|
||||
)
|
||||
tagged_workers = filter_by_user(tagged_workers)
|
||||
worker_keys = (
|
||||
worker_keys.intersection(tagged_workers)
|
||||
if worker_keys
|
||||
else tagged_workers
|
||||
worker_keys = set()
|
||||
for tags, tags_field in (
|
||||
(user_tags, "tags"),
|
||||
(system_tags, "systemtags"),
|
||||
):
|
||||
if not tags:
|
||||
continue
|
||||
|
||||
timestamp = int(time())
|
||||
include, exclude = partition(tags, key=lambda x: x[0] != "-")
|
||||
if include:
|
||||
tagged_workers = set()
|
||||
for tag in include:
|
||||
tagged_workers_key = self._get_tagged_workers_key(
|
||||
company, tags_field, tag
|
||||
)
|
||||
self.redis.zremrangebyscore(
|
||||
tagged_workers_key, min=0, max=timestamp
|
||||
)
|
||||
tagged_workers.update(self.redis.zrange(tagged_workers_key, 0, -1))
|
||||
|
||||
tagged_workers = filter_by_user(tagged_workers)
|
||||
worker_keys = (
|
||||
worker_keys.intersection(tagged_workers)
|
||||
if worker_keys
|
||||
else tagged_workers
|
||||
)
|
||||
if not worker_keys:
|
||||
return []
|
||||
|
||||
if exclude:
|
||||
if not worker_keys:
|
||||
all_workers_key = self._get_all_workers_key(company)
|
||||
self.redis.zremrangebyscore(all_workers_key, min=0, max=timestamp)
|
||||
worker_keys.update(self.redis.zrange(all_workers_key, 0, -1))
|
||||
worker_keys = filter_by_user(worker_keys)
|
||||
if not worker_keys:
|
||||
return []
|
||||
|
||||
for tag in exclude:
|
||||
tagged_workers_key = self._get_tagged_workers_key(
|
||||
company, tags_field, tag[1:]
|
||||
)
|
||||
self.redis.zremrangebyscore(
|
||||
tagged_workers_key, min=0, max=timestamp
|
||||
)
|
||||
worker_keys.difference_update(
|
||||
self.redis.zrange(tagged_workers_key, 0, -1)
|
||||
)
|
||||
if not worker_keys:
|
||||
return []
|
||||
if exclude:
|
||||
if not worker_keys:
|
||||
all_workers_key = self._get_all_workers_key(company)
|
||||
self.redis.zremrangebyscore(
|
||||
all_workers_key, min=0, max=timestamp
|
||||
)
|
||||
worker_keys.update(self.redis.zrange(all_workers_key, 0, -1))
|
||||
worker_keys = filter_by_user(worker_keys)
|
||||
if not worker_keys:
|
||||
return []
|
||||
for tag in exclude:
|
||||
tagged_workers_key = self._get_tagged_workers_key(
|
||||
company, tags_field, tag[1:]
|
||||
)
|
||||
self.redis.zremrangebyscore(
|
||||
tagged_workers_key, min=0, max=timestamp
|
||||
)
|
||||
worker_keys.difference_update(
|
||||
self.redis.zrange(tagged_workers_key, 0, -1)
|
||||
)
|
||||
if not worker_keys:
|
||||
return []
|
||||
else:
|
||||
match = self._get_worker_key(company, user, "*")
|
||||
worker_keys = self.redis.scan_iter(match)
|
||||
|
||||
return list(worker_keys)
|
||||
|
||||
def _get(
|
||||
self,
|
||||
company: str,
|
||||
user: str = "*",
|
||||
user_tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> Sequence[WorkerEntry]:
|
||||
"""Get worker entries matching the company and user, worker patterns"""
|
||||
|
||||
entries = []
|
||||
for key in worker_keys:
|
||||
data = self.redis.get(key)
|
||||
for keys in chunked_iter(
|
||||
self._get_keys(
|
||||
company, user=user, user_tags=user_tags, system_tags=system_tags
|
||||
),
|
||||
1000,
|
||||
):
|
||||
data = self.redis.mget(keys)
|
||||
if data:
|
||||
entries.append(WorkerEntry.from_json(data))
|
||||
entries.extend(WorkerEntry.from_json(d) for d in data if d)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -481,18 +509,17 @@ class WorkerBLL:
|
||||
"""Get the index name suffix for storing current month data"""
|
||||
return datetime.utcnow().strftime("%Y-%m")
|
||||
|
||||
def _log_stats_to_es(
|
||||
def log_stats_to_es(
|
||||
self,
|
||||
company_id: str,
|
||||
company_name: str,
|
||||
worker: str,
|
||||
worker_id: str,
|
||||
timestamp: int,
|
||||
task: str,
|
||||
machine_stats: MachineStats,
|
||||
) -> bool:
|
||||
) -> int:
|
||||
"""
|
||||
Actually writing the worker statistics to Elastic
|
||||
:return: True if successful, False otherwise
|
||||
:return: The amount of logged documents
|
||||
"""
|
||||
es_index = (
|
||||
f"{self._stats.worker_stats_prefix_for_company(company_id)}"
|
||||
@@ -504,8 +531,7 @@ class WorkerBLL:
|
||||
_index=es_index,
|
||||
_source=dict(
|
||||
timestamp=timestamp,
|
||||
worker=worker,
|
||||
company=company_name,
|
||||
worker=worker_id,
|
||||
task=task,
|
||||
category=category,
|
||||
metric=metric,
|
||||
@@ -530,7 +556,7 @@ class WorkerBLL:
|
||||
|
||||
es_res = elasticsearch.helpers.bulk(self.es_client, actions)
|
||||
added, errors = es_res[:2]
|
||||
return (added == len(actions)) and not errors
|
||||
return added
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
|
||||
@@ -215,6 +215,10 @@ class WorkerStats:
|
||||
"date_histogram": {
|
||||
"field": "timestamp",
|
||||
"fixed_interval": f"{interval}s",
|
||||
"extended_bounds": {
|
||||
"min": int(from_date) * 1000,
|
||||
"max": int(to_date) * 1000,
|
||||
}
|
||||
},
|
||||
"aggs": {"workers_count": {"cardinality": {"field": "worker"}}},
|
||||
}
|
||||
|
||||
@@ -41,10 +41,6 @@
|
||||
# controls whether FieldDoesNotExist exception will be raised for any extra attribute existing in stored data
|
||||
# but not declared in a data model
|
||||
strict: false
|
||||
|
||||
aggregate {
|
||||
allow_disk_use: true
|
||||
}
|
||||
}
|
||||
|
||||
elastic {
|
||||
@@ -117,6 +113,10 @@
|
||||
# Timeout in seconds on task status update. If exceeded
|
||||
# then task can be stopped without communicating to the worker
|
||||
task_update_timeout: 600
|
||||
|
||||
# Timeout in seconds for worker registration (or status report). If a worker did not report for this long,
|
||||
# it is discarded from the server's table
|
||||
default_timeout: 600
|
||||
}
|
||||
|
||||
check_for_updates {
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
user_secret: "yfc8KQo*GMXb*9p((qcYC7ByFIpF7I&4VH3BfUYXH%o9vX1ZUZQEEw1Inc)S"
|
||||
revoke_in_fixed_mode: true
|
||||
}
|
||||
services_agent {
|
||||
role: "admin"
|
||||
user_key: "P4BMJA7RK3TKBXGSY8OAA1FA8TOD11"
|
||||
user_secret: "9LsgSfa0SYz0zli1_c500ZcLqanre2xkWOpepyt1w-BKK3_DKPHrtoj3JSHvyy8bIi0"
|
||||
}
|
||||
tests {
|
||||
role: "user"
|
||||
display_name: "Default User"
|
||||
|
||||
@@ -2,3 +2,8 @@ max_page_size: 500
|
||||
|
||||
# expiration time in seconds for the redis scroll states in get_many family of apis
|
||||
scroll_state_expiration_seconds: 600
|
||||
|
||||
allow_disk_use {
|
||||
sort: true
|
||||
aggregate: true
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# if set to True then on task delete/reset external file urls for know storage types are scheduled for async delete
|
||||
# if set to true then on task delete/reset external file urls for known storage types are scheduled for async delete
|
||||
# otherwise they are returned to a client for the client side delete
|
||||
enabled: false
|
||||
enabled: true
|
||||
max_retries: 3
|
||||
retry_timeout_sec: 60
|
||||
|
||||
@@ -9,4 +9,4 @@ fileserver {
|
||||
# Can be in the form <schema>://host:port/path or /path
|
||||
url_prefixes: ["https://files.community-master.hosted.allegro.ai/"]
|
||||
timeout_sec: 300
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ events_retrieval {
|
||||
max_raw_scalars_size: 200000
|
||||
|
||||
scroll_id_key: "cTN5VEtWEC6QrHvUl0FTx9kNyO0CcCK1p57akxma"
|
||||
|
||||
multi_plots_batch_size: 1000
|
||||
}
|
||||
|
||||
# if set then plot str will be checked for the valid json on plot add
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
metadata_values {
|
||||
# maximal amount of distinct model values to retrieve
|
||||
max_count: 100
|
||||
|
||||
# cache ttl sec
|
||||
cache_ttl_sec: 86400
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
tags_cache {
|
||||
expiration_seconds: 3600
|
||||
}
|
||||
download {
|
||||
redis_timeout_sec: 300
|
||||
batch_size: 500
|
||||
max_download_items: 50000
|
||||
max_project_name_length: 60
|
||||
}
|
||||
53
apiserver/config/default/services/storage_credentials.conf
Normal file
53
apiserver/config/default/services/storage_credentials.conf
Normal file
@@ -0,0 +1,53 @@
|
||||
aws {
|
||||
s3 {
|
||||
# S3 credentials, used for read/write access by various SDK elements
|
||||
# default, used for any bucket not specified below
|
||||
key: ""
|
||||
secret: ""
|
||||
region: ""
|
||||
use_credentials_chain: false
|
||||
# Additional ExtraArgs passed to boto3 when uploading files. Can also be set per-bucket under "credentials".
|
||||
extra_args: {}
|
||||
credentials: [
|
||||
# specifies key/secret credentials to use when handling s3 urls (read or write)
|
||||
# {
|
||||
# bucket: "my-bucket-name"
|
||||
# key: "my-access-key"
|
||||
# secret: "my-secret-key"
|
||||
# },
|
||||
{
|
||||
# This will apply to all buckets in this host (unless key/value is specifically provided for a given bucket)
|
||||
host: "localhost:9000"
|
||||
key: "evg_user"
|
||||
secret: "evg_pass"
|
||||
multipart: false
|
||||
secure: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
google.storage {
|
||||
# Default project and credentials file
|
||||
# Will be used when no bucket configuration is found
|
||||
// project: "clearml"
|
||||
// credentials_json: "/path/to/credentials.json"
|
||||
//
|
||||
// # Specific credentials per bucket and sub directory
|
||||
// credentials = [
|
||||
// {
|
||||
// bucket: "my-bucket"
|
||||
// subdir: "path/in/bucket" # Not required
|
||||
// project: "clearml"
|
||||
// credentials_json: "/path/to/credentials.json"
|
||||
// },
|
||||
// ]
|
||||
}
|
||||
azure.storage {
|
||||
# containers: [
|
||||
# {
|
||||
# account_name: "clearml"
|
||||
# account_key: "secret"
|
||||
# # container_name:
|
||||
# }
|
||||
# ]
|
||||
}
|
||||
@@ -11,9 +11,6 @@ non_responsive_tasks_watchdog {
|
||||
multi_task_histogram_limit: 100
|
||||
|
||||
hyperparam_values {
|
||||
# maximal amount of distinct hyperparam values to retrieve
|
||||
max_count: 100
|
||||
|
||||
# max allowed outdate time for the cashed result
|
||||
cache_allowed_outdate_sec: 60
|
||||
|
||||
@@ -26,4 +23,6 @@ hyperparam_values {
|
||||
max_last_metrics: 2000
|
||||
|
||||
# if set then call to tasks.delete/cleanup does not wait for ES events deletion
|
||||
async_events_delete: false
|
||||
async_events_delete: true
|
||||
# do not use async_delete if the deleted task has amount of events lower than this threshold
|
||||
async_events_delete_threshold: 100000
|
||||
|
||||
@@ -16,7 +16,7 @@ from mongoengine.errors import (
|
||||
LookUpError,
|
||||
InvalidQueryError,
|
||||
)
|
||||
from pymongo.errors import PyMongoError, NotMasterError
|
||||
from pymongo.errors import PyMongoError, NotPrimaryError
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
|
||||
@@ -198,7 +198,7 @@ def translate_errors_context(message=None, **kwargs):
|
||||
MongoEngineErrorsHandler.invalid_query_error(e, message, **kwargs)
|
||||
except PyMongoError as e:
|
||||
raise errors.server_error.InternalError(message, err=str(e))
|
||||
except NotMasterError as e:
|
||||
except NotPrimaryError as e:
|
||||
raise errors.server_error.InternalError(message, err=str(e))
|
||||
except MakeGetAllQueryError as e:
|
||||
raise errors.bad_request.ValidationError(e.error, field=e.field)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, defaultdict
|
||||
from datetime import datetime
|
||||
from functools import reduce, partial
|
||||
from typing import (
|
||||
Collection,
|
||||
@@ -11,17 +12,18 @@ from typing import (
|
||||
Mapping,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Generator,
|
||||
)
|
||||
|
||||
import attr
|
||||
from boltons.iterutils import first, partition
|
||||
from dateutil.parser import parse as parse_datetime
|
||||
from mongoengine import Q, Document, ListField, StringField, IntField
|
||||
from mongoengine import Q, Document, ListField, StringField, IntField, QuerySet
|
||||
from pymongo.command_cursor import CommandCursor
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apierrors import errors, APIError
|
||||
from apiserver.apierrors.base import BaseError
|
||||
from apiserver.apierrors.errors.bad_request import FieldsValueError
|
||||
from apiserver.bll.redis_cache_manager import RedisCacheManager
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database import Database
|
||||
@@ -39,7 +41,7 @@ from apiserver.redis_manager import redman
|
||||
from apiserver.utilities.dicts import project_dict, exclude_fields_from_dict
|
||||
|
||||
log = config.logger("dbmodel")
|
||||
|
||||
mongo_conf = config.get("services._mongo")
|
||||
ACCESS_REGEX = re.compile(r"^(?P<prefix>>=|>|<=|<)?(?P<value>.*)$")
|
||||
ACCESS_MODIFIER = {">=": "gte", ">": "gt", "<=": "lte", "<": "lt"}
|
||||
|
||||
@@ -132,84 +134,135 @@ class GetMixin(PropsMixin):
|
||||
self.range_fields = range_fields
|
||||
self.pattern_fields = pattern_fields
|
||||
|
||||
class ListFieldBucketHelper:
|
||||
class NewListFieldBucketHelper:
|
||||
op_prefix = "__$"
|
||||
_legacy_exclude_prefix = "-"
|
||||
_legacy_exclude_mongo_op = "nin"
|
||||
|
||||
default_mongo_op = "in"
|
||||
_ops = {
|
||||
# op -> (mongo_op, sticky)
|
||||
"not": ("nin", False),
|
||||
"nop": (default_mongo_op, False),
|
||||
"all": ("all", True),
|
||||
"and": ("all", True),
|
||||
"any": (default_mongo_op, True),
|
||||
"or": (default_mongo_op, True),
|
||||
_unary_operators = {
|
||||
"__$not": False,
|
||||
}
|
||||
_reset_operator = "__$nop"
|
||||
_operators = {
|
||||
"__$all": Q.AND,
|
||||
"__$and": Q.AND,
|
||||
"__$any": Q.OR,
|
||||
"__$or": Q.OR,
|
||||
}
|
||||
default_operator = Q.OR
|
||||
mongo_modifiers = {
|
||||
# not_all modifier currently not supported due to the backwards compatibility
|
||||
Q.AND: {True: "all", False: "nin"},
|
||||
Q.OR: {True: "in", False: "nin"},
|
||||
}
|
||||
|
||||
def __init__(self, legacy=False):
|
||||
self._current_op = None
|
||||
self._sticky = False
|
||||
self._support_legacy = legacy
|
||||
@attr.s(auto_attribs=True)
|
||||
class Term:
|
||||
operator: str = None
|
||||
reset: bool = False
|
||||
include: bool = True
|
||||
value: str = None
|
||||
|
||||
def __init__(self, field: str, data: Sequence[str], legacy=False):
|
||||
self._field = field
|
||||
self.allow_empty = False
|
||||
self.global_operator = None
|
||||
self.actions = defaultdict(list)
|
||||
self.explicit_operator = False
|
||||
|
||||
def _get_op(self, v: str, translate: bool = False) -> Optional[str]:
|
||||
op = (
|
||||
v[len(self.op_prefix) :] if v and v.startswith(self.op_prefix) else None
|
||||
)
|
||||
if translate:
|
||||
tup = self._ops.get(op, None)
|
||||
return tup[0] if tup else None
|
||||
return op
|
||||
|
||||
def _key(self, v) -> Optional[Union[str, bool]]:
|
||||
if v is None:
|
||||
self.allow_empty = True
|
||||
return None
|
||||
|
||||
op = self._get_op(v)
|
||||
if op is not None:
|
||||
# operator - set state and return None
|
||||
self._current_op, self._sticky = self._ops.get(
|
||||
op, (self.default_mongo_op, self._sticky)
|
||||
)
|
||||
return None
|
||||
elif self._current_op:
|
||||
current_op = self._current_op
|
||||
if not self._sticky:
|
||||
self._current_op = None
|
||||
return current_op
|
||||
elif self._support_legacy and v.startswith(self._legacy_exclude_prefix):
|
||||
self._current_op = None
|
||||
return False
|
||||
|
||||
return self.default_mongo_op
|
||||
|
||||
def get_global_op(self, data: Sequence[str]) -> int:
|
||||
op_to_res = {
|
||||
"in": Q.OR,
|
||||
"all": Q.AND,
|
||||
}
|
||||
data = (x for x in data if x is not None)
|
||||
first_op = (
|
||||
self._get_op(next(data, ""), translate=True) or self.default_mongo_op
|
||||
)
|
||||
return op_to_res.get(first_op, self.default_mongo_op)
|
||||
|
||||
def get_actions(self, data: Sequence[str]) -> Dict[str, List[Union[str, None]]]:
|
||||
actions = {}
|
||||
|
||||
for val in data:
|
||||
key = self._key(val)
|
||||
if key is None:
|
||||
self._support_legacy = legacy
|
||||
current_context = self.default_operator
|
||||
for d in self._get_next_term(data):
|
||||
if d.operator is not None:
|
||||
current_context = d.operator
|
||||
self._support_legacy = False
|
||||
if self.global_operator is None:
|
||||
self.global_operator = d.operator
|
||||
self.explicit_operator = True
|
||||
continue
|
||||
elif self._support_legacy and key is False:
|
||||
key = self._legacy_exclude_mongo_op
|
||||
val = val[len(self._legacy_exclude_prefix) :]
|
||||
actions.setdefault(key, []).append(val)
|
||||
|
||||
return actions
|
||||
if self.global_operator is None:
|
||||
self.global_operator = self.default_operator
|
||||
|
||||
if d.reset:
|
||||
current_context = self.default_operator
|
||||
self._support_legacy = legacy
|
||||
continue
|
||||
|
||||
if d.value is None:
|
||||
self.allow_empty = True
|
||||
continue
|
||||
|
||||
self.actions[self.mongo_modifiers[current_context][d.include]].append(
|
||||
d.value
|
||||
)
|
||||
|
||||
if self.global_operator is None:
|
||||
self.global_operator = self.default_operator
|
||||
|
||||
def _get_next_term(self, data: Sequence[str]) -> Generator[Term, None, None]:
|
||||
unary_operator = None
|
||||
for value in data:
|
||||
if value is None:
|
||||
unary_operator = None
|
||||
yield self.Term()
|
||||
continue
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise FieldsValueError(
|
||||
"invalid value type, string expected",
|
||||
field=self._field,
|
||||
value=str(value),
|
||||
)
|
||||
|
||||
if value == self._reset_operator:
|
||||
unary_operator = None
|
||||
yield self.Term(reset=True)
|
||||
continue
|
||||
|
||||
if value.startswith(self.op_prefix):
|
||||
if unary_operator:
|
||||
raise FieldsValueError(
|
||||
"Value is expected after",
|
||||
field=self._field,
|
||||
operator=unary_operator,
|
||||
)
|
||||
if value in self._unary_operators:
|
||||
unary_operator = value
|
||||
continue
|
||||
|
||||
operator = self._operators.get(value)
|
||||
if operator is None:
|
||||
raise FieldsValueError(
|
||||
"Unsupported operator",
|
||||
field=self._field,
|
||||
operator=value,
|
||||
)
|
||||
yield self.Term(operator=operator)
|
||||
continue
|
||||
|
||||
if (
|
||||
not unary_operator
|
||||
and self._support_legacy
|
||||
and value.startswith("-")
|
||||
):
|
||||
value = value[1:]
|
||||
if not value:
|
||||
raise FieldsValueError(
|
||||
"Missing value after the exclude prefix -",
|
||||
field=self._field,
|
||||
value=value,
|
||||
)
|
||||
yield self.Term(value=value, include=False)
|
||||
continue
|
||||
|
||||
term = self.Term(value=value)
|
||||
if unary_operator:
|
||||
term.include = self._unary_operators[unary_operator]
|
||||
unary_operator = None
|
||||
yield term
|
||||
|
||||
if unary_operator:
|
||||
raise FieldsValueError(
|
||||
"Value is expected after", operator=unary_operator
|
||||
)
|
||||
|
||||
get_all_query_options = QueryParameterOptions()
|
||||
|
||||
@@ -227,8 +280,8 @@ class GetMixin(PropsMixin):
|
||||
cls._cache_manager = RedisCacheManager(
|
||||
state_class=cls.GetManyScrollState,
|
||||
redis=redman.connection("apiserver"),
|
||||
expiration_interval=config.get(
|
||||
"services._mongo.scroll_state_expiration_seconds", 600
|
||||
expiration_interval=mongo_conf.get(
|
||||
"scroll_state_expiration_seconds", 600
|
||||
),
|
||||
)
|
||||
|
||||
@@ -347,97 +400,121 @@ class GetMixin(PropsMixin):
|
||||
parameters_options = parameters_options or cls.get_all_query_options
|
||||
dict_query = {}
|
||||
query = RegexQ()
|
||||
if parameters:
|
||||
parameters = {
|
||||
k: cls._get_fixed_field_value(k, v) for k, v in parameters.items()
|
||||
}
|
||||
opts = parameters_options
|
||||
for field in opts.pattern_fields:
|
||||
pattern = parameters.pop(field, None)
|
||||
if pattern:
|
||||
dict_query[field] = RegexWrapper(pattern)
|
||||
field = None
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if parameters:
|
||||
parameters = {
|
||||
k: cls._get_fixed_field_value(k, v) for k, v in parameters.items()
|
||||
}
|
||||
filters = parameters.pop("filters", {})
|
||||
if not isinstance(filters, dict):
|
||||
raise FieldsValueError(
|
||||
"invalid value type, string expected",
|
||||
field=filters,
|
||||
value=str(filters),
|
||||
)
|
||||
opts = parameters_options
|
||||
for field in opts.pattern_fields:
|
||||
pattern = parameters.pop(field, None)
|
||||
if pattern:
|
||||
dict_query[field] = RegexWrapper(pattern)
|
||||
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.list_fields, parameters=parameters
|
||||
).items():
|
||||
query &= cls.get_list_field_query(field, data)
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.list_fields, parameters=filters
|
||||
).items():
|
||||
query &= cls.get_list_filter_query(field, data)
|
||||
parameters.pop(field, None)
|
||||
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.range_fields, parameters=parameters
|
||||
).items():
|
||||
query &= cls.get_range_field_query(field, data)
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.list_fields, parameters=parameters
|
||||
).items():
|
||||
query &= cls.get_list_field_query(field, data)
|
||||
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.fields or [], parameters=parameters
|
||||
).items():
|
||||
if "._" in field or "_." in field:
|
||||
query &= RegexQ(__raw__={field: data})
|
||||
else:
|
||||
dict_query[field.replace(".", "__")] = data
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.range_fields, parameters=parameters
|
||||
).items():
|
||||
query &= cls.get_range_field_query(field, data)
|
||||
|
||||
for field in opts.datetime_fields or []:
|
||||
data = parameters.pop(field, None)
|
||||
if data is not None:
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
# date time fields also support simplified range queries. Check if this is the case
|
||||
if len(data) == 2 and not any(
|
||||
d.startswith(mod)
|
||||
for d in data
|
||||
if d is not None
|
||||
for mod in ACCESS_MODIFIER
|
||||
):
|
||||
query &= cls.get_range_field_query(field, data)
|
||||
for field, data in cls._pop_matching_params(
|
||||
patterns=opts.fields or [], parameters=parameters
|
||||
).items():
|
||||
if "._" in field or "_." in field:
|
||||
query &= RegexQ(__raw__={field: data})
|
||||
else:
|
||||
for d in data: # type: str
|
||||
m = ACCESS_REGEX.match(d)
|
||||
if not m:
|
||||
continue
|
||||
try:
|
||||
value = parse_datetime(m.group("value"))
|
||||
prefix = m.group("prefix")
|
||||
modifier = ACCESS_MODIFIER.get(prefix)
|
||||
f = (
|
||||
field
|
||||
if not modifier
|
||||
else "__".join((field, modifier))
|
||||
)
|
||||
dict_query[f] = value
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
dict_query[field.replace(".", "__")] = data
|
||||
|
||||
for field, value in parameters.items():
|
||||
for keys, func in cls._multi_field_param_prefix.items():
|
||||
if field not in keys:
|
||||
continue
|
||||
try:
|
||||
data = cls.MultiFieldParameters(**value)
|
||||
except Exception:
|
||||
raise MakeGetAllQueryError("incorrect field format", field)
|
||||
if not data.fields:
|
||||
break
|
||||
if any("._" in f for f in data.fields):
|
||||
q = reduce(
|
||||
lambda a, x: func(
|
||||
a,
|
||||
RegexQ(
|
||||
__raw__={
|
||||
x: {"$regex": data.pattern, "$options": "i"}
|
||||
}
|
||||
for field in opts.datetime_fields or []:
|
||||
data = parameters.pop(field, None)
|
||||
if data is not None:
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
# date time fields also support simplified range queries. Check if this is the case
|
||||
if len(data) == 2 and not any(
|
||||
d.startswith(mod)
|
||||
for d in data
|
||||
if d is not None
|
||||
for mod in ACCESS_MODIFIER
|
||||
):
|
||||
query &= cls.get_range_field_query(field, data)
|
||||
else:
|
||||
for d in data: # type: str
|
||||
m = ACCESS_REGEX.match(d)
|
||||
if not m:
|
||||
continue
|
||||
try:
|
||||
value = parse_datetime(m.group("value"))
|
||||
prefix = m.group("prefix")
|
||||
modifier = ACCESS_MODIFIER.get(prefix)
|
||||
f = (
|
||||
field
|
||||
if not modifier
|
||||
else "__".join((field, modifier))
|
||||
)
|
||||
dict_query[f] = value
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
|
||||
for field, value in parameters.items():
|
||||
for keys, func in cls._multi_field_param_prefix.items():
|
||||
if field not in keys:
|
||||
continue
|
||||
try:
|
||||
data = cls.MultiFieldParameters(**value)
|
||||
except Exception:
|
||||
raise MakeGetAllQueryError("incorrect field format", field)
|
||||
if not data.fields:
|
||||
break
|
||||
if any("._" in f for f in data.fields):
|
||||
q = reduce(
|
||||
lambda a, x: func(
|
||||
a,
|
||||
RegexQ(
|
||||
__raw__={
|
||||
x: {"$regex": data.pattern, "$options": "i"}
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
data.fields,
|
||||
RegexQ(),
|
||||
)
|
||||
else:
|
||||
regex = RegexWrapper(data.pattern, flags=re.IGNORECASE)
|
||||
sep_fields = [f.replace(".", "__") for f in data.fields]
|
||||
q = reduce(
|
||||
lambda a, x: func(a, RegexQ(**{x: regex})),
|
||||
sep_fields,
|
||||
RegexQ(),
|
||||
)
|
||||
query = query & q
|
||||
data.fields,
|
||||
RegexQ(),
|
||||
)
|
||||
else:
|
||||
regex = RegexWrapper(data.pattern, flags=re.IGNORECASE)
|
||||
sep_fields = [f.replace(".", "__") for f in data.fields]
|
||||
q = reduce(
|
||||
lambda a, x: func(a, RegexQ(**{x: regex})),
|
||||
sep_fields,
|
||||
RegexQ(),
|
||||
)
|
||||
query = query & q
|
||||
except APIError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise errors.bad_request.FieldsValueError(
|
||||
"failed parsing query field",
|
||||
error=str(ex),
|
||||
**({"field": field} if field else {}),
|
||||
)
|
||||
|
||||
return query & RegexQ(**dict_query)
|
||||
|
||||
@@ -472,6 +549,135 @@ class GetMixin(PropsMixin):
|
||||
|
||||
return q
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class ListQueryFilter:
|
||||
"""
|
||||
Deserialize filters data and build db_query object that represents it with the corresponding
|
||||
mongo engine operations
|
||||
Each part has include and exclude lists that map to mongoengine operations as following:
|
||||
"any"
|
||||
- include -> 'in'
|
||||
- exclude -> 'not_all'
|
||||
- combined by 'or' operation
|
||||
"all"
|
||||
- include -> 'all'
|
||||
- exclude -> 'nin'
|
||||
- combined by 'and' operation
|
||||
"op" optional parameter for combining "and" and "all" parts. Can be "and" or "or". The default is "and"
|
||||
"""
|
||||
|
||||
_and_op = "and"
|
||||
_or_op = "or"
|
||||
_allowed_op = [_and_op, _or_op]
|
||||
_db_modifiers: Mapping = {
|
||||
(Q.OR, True): "in",
|
||||
(Q.OR, False): "not__all",
|
||||
(Q.AND, True): "all",
|
||||
(Q.AND, False): "nin",
|
||||
}
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class ListFilter:
|
||||
include: Sequence[str] = []
|
||||
exclude: Sequence[str] = []
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Mapping):
|
||||
if d is None:
|
||||
return None
|
||||
return cls(**d)
|
||||
|
||||
any: ListFilter = attr.ib(converter=ListFilter.from_dict, default=None)
|
||||
all: ListFilter = attr.ib(converter=ListFilter.from_dict, default=None)
|
||||
op: str = attr.ib(default="and")
|
||||
db_query: dict = attr.ib(init=False)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
@op.validator
|
||||
def op_validator(self, _, value):
|
||||
if value not in self._allowed_op:
|
||||
raise ValueError(
|
||||
f"Invalid list query filter operator: {value}. "
|
||||
f"Should be one of {str(self._allowed_op)}"
|
||||
)
|
||||
|
||||
@property
|
||||
def and_op(self) -> bool:
|
||||
return self.op == self._and_op
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
self.db_query = {}
|
||||
for op, conditions in ((Q.OR, self.any), (Q.AND, self.all)):
|
||||
if not conditions:
|
||||
continue
|
||||
|
||||
operations = {}
|
||||
for vals, include in (
|
||||
(conditions.include, True),
|
||||
(conditions.exclude, False),
|
||||
):
|
||||
if not vals:
|
||||
continue
|
||||
operations[self._db_modifiers[(op, include)]] = list(set(vals))
|
||||
|
||||
self.db_query[op] = operations
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, field, data: Mapping):
|
||||
if not isinstance(data, dict):
|
||||
raise errors.bad_request.ValidationError(
|
||||
"invalid filter for field, dictionary expected",
|
||||
field=field,
|
||||
value=str(data),
|
||||
)
|
||||
|
||||
try:
|
||||
return cls(**data)
|
||||
except Exception as ex:
|
||||
raise errors.bad_request.ValidationError(
|
||||
field=field,
|
||||
value=str(ex),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_list_filter_query(
|
||||
cls, field: str, data: Mapping
|
||||
) -> Union[RegexQ, RegexQCombination]:
|
||||
if not data:
|
||||
return RegexQ()
|
||||
|
||||
filter_ = cls.ListQueryFilter.from_data(field, data)
|
||||
|
||||
mongoengine_field = field.replace(".", "__")
|
||||
queries = []
|
||||
for op, actions in filter_.db_query.items():
|
||||
if not actions:
|
||||
continue
|
||||
|
||||
ops = []
|
||||
for action, vals in actions.items():
|
||||
if not vals:
|
||||
continue
|
||||
|
||||
ops.append(RegexQ(**{f"{mongoengine_field}__{action}": vals}))
|
||||
|
||||
if not ops:
|
||||
continue
|
||||
|
||||
if len(ops) == 1:
|
||||
queries.extend(ops)
|
||||
continue
|
||||
|
||||
queries.append(RegexQCombination(operation=op, children=ops))
|
||||
|
||||
if not queries:
|
||||
return RegexQ()
|
||||
if len(queries) == 1:
|
||||
return queries[0]
|
||||
|
||||
operation = Q.AND if filter_.and_op else Q.OR
|
||||
return RegexQCombination(operation=operation, children=queries)
|
||||
|
||||
@classmethod
|
||||
def get_list_field_query(cls, field: str, data: Sequence[Optional[str]]) -> RegexQ:
|
||||
"""
|
||||
@@ -486,15 +692,15 @@ class GetMixin(PropsMixin):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = [data]
|
||||
|
||||
helper = cls.ListFieldBucketHelper(legacy=True)
|
||||
global_op = helper.get_global_op(data)
|
||||
actions = helper.get_actions(data)
|
||||
helper = cls.NewListFieldBucketHelper(field, data=data, legacy=True)
|
||||
global_op = helper.global_operator
|
||||
actions = helper.actions
|
||||
|
||||
mongoengine_field = field.replace(".", "__")
|
||||
|
||||
queries = [
|
||||
RegexQ(**{f"{mongoengine_field}__{action}": list(set(actions[action]))})
|
||||
for action in filter(None, actions)
|
||||
RegexQ(**{f"{mongoengine_field}__{action}": list(set(values))})
|
||||
for action, values in actions.items()
|
||||
]
|
||||
|
||||
if not queries:
|
||||
@@ -555,7 +761,7 @@ class GetMixin(PropsMixin):
|
||||
if start is not None:
|
||||
return start, cls.validate_scroll_size(parameters)
|
||||
|
||||
max_page_size = config.get("services._mongo.max_page_size", 500)
|
||||
max_page_size = mongo_conf.get("max_page_size", 500)
|
||||
page = parameters.get("page", default_page)
|
||||
if page is not None and page < 0:
|
||||
raise errors.bad_request.ValidationError("page must be >=0", field="page")
|
||||
@@ -580,7 +786,7 @@ class GetMixin(PropsMixin):
|
||||
|
||||
@classmethod
|
||||
def get_projection(cls, parameters, override_projection=None, **__):
|
||||
""" Extract a projection list from the provided dictionary. Supports an override projection. """
|
||||
"""Extract a projection list from the provided dictionary. Supports an override projection."""
|
||||
if override_projection is not None:
|
||||
return override_projection
|
||||
if not parameters:
|
||||
@@ -594,7 +800,8 @@ class GetMixin(PropsMixin):
|
||||
"""Return include and exclude lists based on passed projection and class definition"""
|
||||
if projection:
|
||||
include, exclude = partition(
|
||||
projection, key=lambda x: x[0] != ProjectionHelper.exclusion_prefix,
|
||||
projection,
|
||||
key=lambda x: x[0] != ProjectionHelper.exclusion_prefix,
|
||||
)
|
||||
else:
|
||||
include, exclude = [], []
|
||||
@@ -733,7 +940,9 @@ class GetMixin(PropsMixin):
|
||||
@classmethod
|
||||
def _get_collation_override(cls, field: str) -> Optional[dict]:
|
||||
return first(
|
||||
v for k, v in cls._field_collation_overrides.items() if field.startswith(k)
|
||||
v
|
||||
for k, v in cls._field_collation_overrides.items()
|
||||
if field.startswith(k) or field.startswith(f"-{k}")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -839,7 +1048,9 @@ class GetMixin(PropsMixin):
|
||||
projection_fields=projection_fields,
|
||||
)
|
||||
return cls.get_data_with_scroll_support(
|
||||
query_dict=query_dict, data_getter=data_getter, ret_params=ret_params,
|
||||
query_dict=query_dict,
|
||||
data_getter=data_getter,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
|
||||
return cls._get_many_no_company(
|
||||
@@ -852,7 +1063,9 @@ class GetMixin(PropsMixin):
|
||||
|
||||
@classmethod
|
||||
def get_many_public(
|
||||
cls, query: Q = None, projection: Collection[str] = None,
|
||||
cls,
|
||||
query: Q = None,
|
||||
projection: Collection[str] = None,
|
||||
):
|
||||
"""
|
||||
Fetch all public documents matching a provided query.
|
||||
@@ -865,6 +1078,13 @@ class GetMixin(PropsMixin):
|
||||
|
||||
return cls._get_many_no_company(query=_query, override_projection=projection)
|
||||
|
||||
@staticmethod
|
||||
def _get_qs_with_ordering(qs: QuerySet, order_by: Sequence):
|
||||
disk_use_setting = mongo_conf.get("allow_disk_use.sort", None)
|
||||
if disk_use_setting is not None:
|
||||
qs = qs.allow_disk_use(disk_use_setting)
|
||||
return qs.order_by(*order_by)
|
||||
|
||||
@classmethod
|
||||
def _get_many_no_company(
|
||||
cls: Union["GetMixin", Document],
|
||||
@@ -906,7 +1126,7 @@ class GetMixin(PropsMixin):
|
||||
qs = qs.search_text(search_text)
|
||||
if order_by:
|
||||
# add ordering
|
||||
qs = qs.order_by(*order_by)
|
||||
qs = cls._get_qs_with_ordering(qs, order_by)
|
||||
|
||||
if include:
|
||||
# add projection
|
||||
@@ -998,7 +1218,7 @@ class GetMixin(PropsMixin):
|
||||
res = cls._get_queries_for_order_field(query, order_field)
|
||||
if res:
|
||||
query_sets = [cls.objects(q) for q in res]
|
||||
query_sets = [qs.order_by(*order_by) for qs in query_sets]
|
||||
query_sets = [cls._get_qs_with_ordering(qs, order_by) for qs in query_sets]
|
||||
if order_field and not override_collation:
|
||||
override_collation = cls._get_collation_override(order_field)
|
||||
|
||||
@@ -1138,7 +1358,7 @@ class UpdateMixin(object):
|
||||
|
||||
|
||||
class DbModelMixin(GetMixin, ProperDictMixin, UpdateMixin):
|
||||
""" Provide convenience methods for a subclass of mongoengine.Document """
|
||||
"""Provide convenience methods for a subclass of mongoengine.Document"""
|
||||
|
||||
@classmethod
|
||||
def aggregate(
|
||||
@@ -1158,7 +1378,7 @@ class DbModelMixin(GetMixin, ProperDictMixin, UpdateMixin):
|
||||
kwargs.update(
|
||||
allowDiskUse=allow_disk_use
|
||||
if allow_disk_use is not None
|
||||
else config.get("apiserver.mongo.aggregate.allow_disk_use", True)
|
||||
else mongo_conf.get("allow_disk_use.aggregate", True)
|
||||
)
|
||||
return cls.objects.aggregate(pipeline, **kwargs)
|
||||
|
||||
@@ -1166,25 +1386,31 @@ class DbModelMixin(GetMixin, ProperDictMixin, UpdateMixin):
|
||||
def set_public(
|
||||
cls: Type[Document],
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
ids: Sequence[str],
|
||||
invalid_cls: Type[BaseError],
|
||||
enabled: bool = True,
|
||||
):
|
||||
if enabled:
|
||||
items = list(cls.objects(id__in=ids, company=company_id).only("id"))
|
||||
update = dict(set__company_origin=company_id, set__company="")
|
||||
update: dict = dict(set__company_origin=company_id, set__company="")
|
||||
else:
|
||||
items = list(
|
||||
cls.objects(
|
||||
id__in=ids, company__in=(None, ""), company_origin=company_id
|
||||
).only("id")
|
||||
)
|
||||
update = dict(set__company=company_id, unset__company_origin=1)
|
||||
update: dict = dict(set__company=company_id, unset__company_origin=1)
|
||||
|
||||
if len(items) < len(ids):
|
||||
missing = tuple(set(ids).difference(i.id for i in items))
|
||||
raise invalid_cls(ids=missing)
|
||||
|
||||
if hasattr(cls, "last_change"):
|
||||
update["set__last_change"] = datetime.utcnow()
|
||||
if hasattr(cls, "last_changed_by"):
|
||||
update["set__last_changed_by"] = user_id
|
||||
|
||||
return {"updated": cls.objects(id__in=ids).update(**update)}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ from mongoengine import (
|
||||
DateTimeField,
|
||||
BooleanField,
|
||||
EmbeddedDocumentField,
|
||||
IntField,
|
||||
ListField,
|
||||
)
|
||||
|
||||
from apiserver.database import Database, strict
|
||||
@@ -17,12 +19,14 @@ from apiserver.database.model.base import GetMixin
|
||||
from apiserver.database.model.metadata import MetadataItem
|
||||
from apiserver.database.model.model_labels import ModelLabels
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.task.metrics import MetricEvent
|
||||
from apiserver.database.model.task.task import Task
|
||||
|
||||
|
||||
class Model(AttributedDocument):
|
||||
_field_collation_overrides = {
|
||||
"metadata.": AttributedDocument._numeric_locale,
|
||||
"last_metrics.": AttributedDocument._numeric_locale,
|
||||
}
|
||||
|
||||
meta = {
|
||||
@@ -67,6 +71,7 @@ class Model(AttributedDocument):
|
||||
"parent",
|
||||
"metadata.*",
|
||||
),
|
||||
range_fields=("last_metrics.*", "last_iteration"),
|
||||
datetime_fields=("last_update",),
|
||||
)
|
||||
|
||||
@@ -85,6 +90,8 @@ class Model(AttributedDocument):
|
||||
labels = ModelLabels()
|
||||
ready = BooleanField(required=True)
|
||||
last_update = DateTimeField()
|
||||
last_change = DateTimeField()
|
||||
last_changed_by = StringField()
|
||||
ui_cache = SafeDictField(
|
||||
default=dict, user_set_allowed=True, exclude_by_default=True
|
||||
)
|
||||
@@ -92,6 +99,9 @@ class Model(AttributedDocument):
|
||||
metadata = SafeMapField(
|
||||
field=EmbeddedDocumentField(MetadataItem), user_set_allowed=True
|
||||
)
|
||||
last_iteration = IntField(default=0)
|
||||
last_metrics = SafeMapField(field=SafeMapField(EmbeddedDocumentField(MetricEvent)))
|
||||
unique_metrics = ListField(StringField(required=True), exclude_by_default=True)
|
||||
|
||||
def get_index_company(self) -> str:
|
||||
return self.company or self.company_origin or ""
|
||||
|
||||
@@ -4,6 +4,7 @@ from mongoengine import (
|
||||
DynamicField,
|
||||
LongField,
|
||||
EmbeddedDocumentField,
|
||||
IntField,
|
||||
)
|
||||
|
||||
from apiserver.database.fields import SafeMapField
|
||||
@@ -19,7 +20,9 @@ class MetricEvent(EmbeddedDocument):
|
||||
variant = StringField(required=True)
|
||||
value = DynamicField(required=True)
|
||||
min_value = DynamicField() # for backwards compatibility reasons
|
||||
min_value_iteration = IntField()
|
||||
max_value = DynamicField() # for backwards compatibility reasons
|
||||
max_value_iteration = IntField()
|
||||
|
||||
|
||||
class EventStats(EmbeddedDocument):
|
||||
|
||||
@@ -19,6 +19,7 @@ from apiserver.database.fields import (
|
||||
SafeSortedListField,
|
||||
EmbeddedDocumentListField,
|
||||
NullableStringField,
|
||||
NoneType,
|
||||
)
|
||||
from apiserver.database.model import AttributedDocument
|
||||
from apiserver.database.model.base import ProperDictMixin, GetMixin
|
||||
@@ -89,7 +90,9 @@ class Artifact(EmbeddedDocument):
|
||||
content_size = LongField()
|
||||
timestamp = LongField()
|
||||
type_data = EmbeddedDocumentField(ArtifactTypeData)
|
||||
display_data = SafeSortedListField(ListField(UnionField((int, float, str))))
|
||||
display_data = SafeSortedListField(
|
||||
ListField(UnionField((int, float, str, NoneType)))
|
||||
)
|
||||
|
||||
|
||||
class ParamsItem(EmbeddedDocument, ProperDictMixin):
|
||||
@@ -149,6 +152,7 @@ class TaskType(object):
|
||||
application = "application"
|
||||
monitor = "monitor"
|
||||
controller = "controller"
|
||||
report = "report"
|
||||
optimizer = "optimizer"
|
||||
service = "service"
|
||||
qc = "qc"
|
||||
@@ -195,6 +199,7 @@ class Task(AttributedDocument):
|
||||
"$name",
|
||||
"$id",
|
||||
"$comment",
|
||||
"$report",
|
||||
"$models.input.model",
|
||||
"$models.output.model",
|
||||
"$script.repository",
|
||||
@@ -205,6 +210,7 @@ class Task(AttributedDocument):
|
||||
"name": 10,
|
||||
"id": 10,
|
||||
"comment": 10,
|
||||
"report": 10,
|
||||
"models.output.model": 2,
|
||||
"models.input.model": 2,
|
||||
"script.repository": 1,
|
||||
@@ -224,10 +230,12 @@ class Task(AttributedDocument):
|
||||
"project",
|
||||
"parent",
|
||||
"hyperparams.*",
|
||||
"execution.queue",
|
||||
),
|
||||
range_fields=("started", "active_duration", "last_metrics.*", "last_iteration"),
|
||||
datetime_fields=("status_changed", "last_update"),
|
||||
pattern_fields=("name", "comment"),
|
||||
pattern_fields=("name", "comment", "report"),
|
||||
fields=("runtime.*", "models.input.model"),
|
||||
)
|
||||
|
||||
id = StringField(primary_key=True)
|
||||
@@ -241,6 +249,8 @@ class Task(AttributedDocument):
|
||||
status_message = StringField(user_set_allowed=True)
|
||||
status_changed = DateTimeField()
|
||||
comment = StringField(user_set_allowed=True)
|
||||
report = StringField()
|
||||
report_assets = ListField(StringField())
|
||||
created = DateTimeField(required=True, user_set_allowed=True)
|
||||
started = DateTimeField()
|
||||
completed = DateTimeField()
|
||||
@@ -262,7 +272,7 @@ class Task(AttributedDocument):
|
||||
unique_metrics = ListField(StringField(required=True), exclude_by_default=True)
|
||||
metric_stats = SafeMapField(field=EmbeddedDocumentField(MetricEventStats))
|
||||
company_origin = StringField(exclude_by_default=True)
|
||||
duration = IntField() # task duration in seconds
|
||||
duration = IntField() # obsolete, do not use
|
||||
hyperparams = SafeMapField(field=SafeMapField(EmbeddedDocumentField(ParamsItem)))
|
||||
configuration = SafeMapField(field=EmbeddedDocumentField(ConfigurationItem))
|
||||
runtime = SafeDictField(default=dict)
|
||||
@@ -271,6 +281,7 @@ class Task(AttributedDocument):
|
||||
enqueue_status = StringField(
|
||||
choices=get_options(TaskStatus), exclude_by_default=True
|
||||
)
|
||||
last_changed_by = StringField()
|
||||
|
||||
def get_index_company(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -8,6 +8,9 @@ from apiserver.database.model import AttributedDocument
|
||||
|
||||
class StorageType(str, Enum):
|
||||
fileserver = "fileserver"
|
||||
s3 = "s3"
|
||||
azure = "azure"
|
||||
gs = "gs"
|
||||
unknown = "unknown"
|
||||
|
||||
|
||||
@@ -32,10 +35,8 @@ class UrlToDelete(AttributedDocument):
|
||||
"strict": strict,
|
||||
"indexes": [
|
||||
("company", "user", "task"),
|
||||
"storage_type",
|
||||
"created",
|
||||
"retry_count",
|
||||
"type",
|
||||
("company", "storage_type", "url"),
|
||||
("status", "retry_count", "storage_type"),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from itertools import groupby, chain
|
||||
from typing import Sequence, Dict, Callable
|
||||
|
||||
from boltons import iterutils
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.props import PropsMixin
|
||||
|
||||
SEP = "."
|
||||
max_items_per_fetch = config.get("services._mongo.max_page_size", 500)
|
||||
|
||||
|
||||
class _ReferenceProxy(dict):
|
||||
@@ -278,10 +282,11 @@ class ProjectionHelper(object):
|
||||
doc_only = list(filter(None, data["only"]))
|
||||
doc_only = list({"id"} | set(doc_only)) if doc_only else None
|
||||
|
||||
for res in projection_func(
|
||||
doc_type=doc_type, projection=doc_only, ids=ids
|
||||
):
|
||||
self._proxy_manager.update(res)
|
||||
for ids_chunk in iterutils.chunked_iter(ids, max_items_per_fetch):
|
||||
for res in projection_func(
|
||||
doc_type=doc_type, projection=doc_only, ids=ids_chunk
|
||||
):
|
||||
self._proxy_manager.update(res)
|
||||
|
||||
if len(ref_projection) == 1:
|
||||
do_projection(items[0])
|
||||
|
||||
19
apiserver/documentation/api_versions.md
Normal file
19
apiserver/documentation/api_versions.md
Normal file
@@ -0,0 +1,19 @@
|
||||
### Supported api versions
|
||||
|
||||
| Release | ApiVersion |
|
||||
|---------|------------|
|
||||
| v1.13 | 2.27 |
|
||||
| v1.12 | 2.26 |
|
||||
| v1.11 | 2.25 |
|
||||
| v1.10 | 2.24 |
|
||||
| v1.9 | 2.23 |
|
||||
| v1.8 | 2.22 |
|
||||
| v1.7 | 2.21 |
|
||||
| v1.6 | 2.20 |
|
||||
| v1.5 | 2.19 |
|
||||
| v1.4 | 2.18 |
|
||||
| v1.3 | 2.17 |
|
||||
| v1.2 | 2.16 |
|
||||
| v1.1 | 2.15 |
|
||||
| v1.0 | 2.14 |
|
||||
| v0.17 | 2.13 |
|
||||
@@ -21,7 +21,7 @@ def apply_mappings_to_cluster(
|
||||
with f.open() as json_data:
|
||||
data = json.load(json_data)
|
||||
template_name = f.stem
|
||||
res = es.indices.put_template(template_name, body=data)
|
||||
res = es.indices.put_template(name=template_name, body=data)
|
||||
return {"mapping": template_name, "result": res}
|
||||
|
||||
p = HERE / "mappings"
|
||||
|
||||
@@ -85,7 +85,7 @@ def check_elastic_empty() -> bool:
|
||||
es = Elasticsearch(
|
||||
hosts=cluster_conf.get("hosts", None),
|
||||
http_auth=es_factory.get_credentials("events", cluster_conf),
|
||||
**cluster_conf.get("args", {})
|
||||
**cluster_conf.get("args", {}),
|
||||
)
|
||||
return not es.indices.get_template(name="events*")
|
||||
except exceptions.NotFoundError as ex:
|
||||
@@ -115,5 +115,7 @@ def init_es_data():
|
||||
args = cluster_conf.get("args", {})
|
||||
http_auth = es_factory.get_credentials(name)
|
||||
|
||||
res = apply_mappings_to_cluster(hosts_config, name, es_args=args, http_auth=http_auth)
|
||||
res = apply_mappings_to_cluster(
|
||||
hosts_config, name, es_args=args, http_auth=http_auth
|
||||
)
|
||||
log.info(res)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"index_patterns": "events-*",
|
||||
"settings": {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
"number_of_replicas": 0,
|
||||
"number_of_shards": 1
|
||||
},
|
||||
"mappings": {
|
||||
"_source": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"index_patterns": "queue_metrics_*",
|
||||
"settings": {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
"number_of_replicas": 0,
|
||||
"number_of_shards": 1
|
||||
},
|
||||
"mappings": {
|
||||
"_source": {
|
||||
@@ -20,6 +20,9 @@
|
||||
},
|
||||
"queue_length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"company_id": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"index_patterns": "worker_stats_*",
|
||||
"settings": {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
"number_of_replicas": 0,
|
||||
"number_of_shards": 1
|
||||
},
|
||||
"mappings": {
|
||||
"_source": {
|
||||
@@ -32,6 +32,9 @@
|
||||
},
|
||||
"task": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"company_id": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class MissingPasswordForElasticUser(Exception):
|
||||
|
||||
class ESFactory:
|
||||
@classmethod
|
||||
def connect(cls, cluster_name):
|
||||
def connect(cls, cluster_name) -> Elasticsearch:
|
||||
"""
|
||||
Returns the es client for the cluster.
|
||||
Connects to the cluster if did not connect previously
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from argparse import ArgumentParser
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
@@ -5,38 +7,28 @@ from functools import partial
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
from typing import Sequence, Tuple
|
||||
from typing import Sequence, Optional, Tuple, Mapping, TypeVar, Hashable, Generic
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import boto3
|
||||
import requests
|
||||
from azure.storage.blob import ContainerClient, PartialBatchErrorException
|
||||
from boltons.iterutils import bucketize, chunked_iter
|
||||
from furl import furl
|
||||
from google.cloud import storage as google_storage
|
||||
from mongoengine import Q
|
||||
from mypy_boto3_s3.service_resource import Bucket as AWSBucket
|
||||
|
||||
from apiserver.bll.storage import StorageBLL
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database import db
|
||||
from apiserver.database.model.url_to_delete import (
|
||||
UrlToDelete,
|
||||
DeletionStatus,
|
||||
StorageType,
|
||||
)
|
||||
from apiserver.database.model.url_to_delete import UrlToDelete, StorageType, DeletionStatus
|
||||
|
||||
log = config.logger(f"JOB-{Path(__file__).name}")
|
||||
conf = config.get("services.async_urls_delete")
|
||||
max_retries = conf.get("max_retries", 3)
|
||||
retry_timeout = timedelta(seconds=conf.get("retry_timeout_sec", 60))
|
||||
fileserver_timeout = conf.get("fileserver.timeout_sec", 300)
|
||||
UrlPrefix = Tuple[str, str]
|
||||
|
||||
|
||||
def validate_fileserver_access(fileserver_host: str) -> str:
|
||||
fileserver_host = fileserver_host or config.get("hosts.fileserver", None)
|
||||
if not fileserver_host:
|
||||
log.error(f"Fileserver host not configured")
|
||||
exit(1)
|
||||
|
||||
res = requests.get(url=fileserver_host)
|
||||
res.raise_for_status()
|
||||
|
||||
return fileserver_host
|
||||
storage_bll = StorageBLL()
|
||||
|
||||
|
||||
def mark_retry_failed(ids: Sequence[str], reason: str):
|
||||
@@ -58,112 +50,517 @@ def mark_failed(query: Q, reason: str):
|
||||
)
|
||||
|
||||
|
||||
def delete_fileserver_urls(
|
||||
urls_query: Q, fileserver_host: str, url_prefixes: Sequence[UrlPrefix]
|
||||
):
|
||||
to_delete = list(UrlToDelete.objects(urls_query).limit(10000))
|
||||
def scheme_prefix(scheme: str) -> str:
|
||||
return str(furl(scheme=scheme, netloc=""))
|
||||
|
||||
|
||||
T = TypeVar("T", bound=Hashable)
|
||||
|
||||
|
||||
class Storage(Generic[T], metaclass=ABCMeta):
|
||||
class Client(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def chunk_size(self) -> int:
|
||||
pass
|
||||
|
||||
def get_path(self, url: UrlToDelete) -> str:
|
||||
pass
|
||||
|
||||
def delete_many(
|
||||
self, paths: Sequence[str]
|
||||
) -> Tuple[Sequence[str], Mapping[str, Sequence[str]]]:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
pass
|
||||
|
||||
def group_urls(
|
||||
self, urls: Sequence[UrlToDelete]
|
||||
) -> Mapping[T, Sequence[UrlToDelete]]:
|
||||
pass
|
||||
|
||||
def get_client(self, base: T, urls: Sequence[UrlToDelete]) -> Client:
|
||||
pass
|
||||
|
||||
|
||||
def delete_urls(urls_query: Q, storage: Storage):
|
||||
to_delete = list(UrlToDelete.objects(urls_query).order_by("url").limit(10000))
|
||||
if not to_delete:
|
||||
return
|
||||
|
||||
def resolve_path(url_: UrlToDelete) -> str:
|
||||
parsed = furl(url_.url)
|
||||
url_host = f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme else None
|
||||
url_path = str(parsed.path)
|
||||
grouped_urls = storage.group_urls(to_delete)
|
||||
for base, urls in grouped_urls.items():
|
||||
if not base:
|
||||
msg = f"Invalid {storage.name} url or missing {storage.name} configuration for account"
|
||||
mark_failed(
|
||||
Q(id__in=[url.id for url in urls]), msg,
|
||||
)
|
||||
log.warning(
|
||||
f"Failed to delete {len(urls)} files from {storage.name} due to: {msg}"
|
||||
)
|
||||
continue
|
||||
|
||||
for host, path_prefix in url_prefixes:
|
||||
try:
|
||||
client = storage.get_client(base, urls)
|
||||
except Exception as ex:
|
||||
failed = [url.id for url in urls]
|
||||
mark_retry_failed(failed, reason=str(ex))
|
||||
log.warning(
|
||||
f"Failed to delete {len(failed)} files from {storage.name} due to: {str(ex)}"
|
||||
)
|
||||
continue
|
||||
|
||||
for chunk in chunked_iter(urls, client.chunk_size):
|
||||
paths = []
|
||||
path_to_id_mapping = defaultdict(list)
|
||||
ids_to_delete = set()
|
||||
for url in chunk:
|
||||
try:
|
||||
path = client.get_path(url)
|
||||
except Exception as ex:
|
||||
err = str(ex)
|
||||
mark_failed(Q(id=url.id), err)
|
||||
log.warning(f"Error getting path for {url.url}: {err}")
|
||||
continue
|
||||
|
||||
paths.append(path)
|
||||
path_to_id_mapping[path].append(url.id)
|
||||
ids_to_delete.add(url.id)
|
||||
|
||||
if not paths:
|
||||
continue
|
||||
|
||||
try:
|
||||
deleted_paths, errors = client.delete_many(paths)
|
||||
except Exception as ex:
|
||||
mark_retry_failed([url.id for url in urls], str(ex))
|
||||
log.warning(
|
||||
f"Error deleting {len(paths)} files from {storage.name}: {str(ex)}"
|
||||
)
|
||||
continue
|
||||
|
||||
failed_ids = set()
|
||||
for reason, err_paths in errors.items():
|
||||
error_ids = set(
|
||||
chain.from_iterable(
|
||||
path_to_id_mapping.get(p, []) for p in err_paths
|
||||
)
|
||||
)
|
||||
mark_retry_failed(list(error_ids), reason)
|
||||
log.warning(
|
||||
f"Failed to delete {len(error_ids)} files from {storage.name} storage due to: {reason}"
|
||||
)
|
||||
failed_ids.update(error_ids)
|
||||
|
||||
deleted_ids = set(
|
||||
chain.from_iterable(
|
||||
path_to_id_mapping.get(p, []) for p in deleted_paths
|
||||
)
|
||||
)
|
||||
if deleted_ids:
|
||||
UrlToDelete.objects(id__in=list(deleted_ids)).delete()
|
||||
log.info(
|
||||
f"{len(deleted_ids)} files deleted from {storage.name} storage"
|
||||
)
|
||||
|
||||
missing_ids = ids_to_delete - deleted_ids - failed_ids
|
||||
if missing_ids:
|
||||
mark_retry_failed(list(missing_ids), "Not succeeded")
|
||||
|
||||
|
||||
class FileserverStorage(Storage):
|
||||
class Client(Storage.Client):
|
||||
timeout = conf.get("fileserver.timeout_sec", 300)
|
||||
|
||||
def __init__(self, session: requests.Session, host: str):
|
||||
self.session = session
|
||||
self.delete_url = furl(host).add(path="delete_many").url
|
||||
|
||||
@property
|
||||
def chunk_size(self) -> int:
|
||||
return 10000
|
||||
|
||||
def get_path(self, url: UrlToDelete) -> str:
|
||||
path = url.url.strip("/")
|
||||
if not path:
|
||||
raise ValueError("Empty path")
|
||||
|
||||
return path
|
||||
|
||||
def delete_many(
|
||||
self, paths: Sequence[str]
|
||||
) -> Tuple[Sequence[str], Mapping[str, Sequence[str]]]:
|
||||
res = self.session.post(
|
||||
url=self.delete_url, json={"files": list(paths)}, timeout=self.timeout
|
||||
)
|
||||
res.raise_for_status()
|
||||
res_data = res.json()
|
||||
return list(res_data.get("deleted", {})), res_data.get("errors", {})
|
||||
|
||||
def __init__(self, company: str, fileserver_host: str = None):
|
||||
fileserver_host = fileserver_host or config.get("hosts.fileserver", None)
|
||||
self.host = fileserver_host.rstrip("/")
|
||||
if not self.host:
|
||||
log.warning(f"Fileserver host not configured")
|
||||
|
||||
def _parse_url_prefix(prefix) -> Tuple[str, str]:
|
||||
url = furl(prefix)
|
||||
host = f"{url.scheme}://{url.netloc}" if url.scheme else None
|
||||
return host, str(url.path).rstrip("/")
|
||||
|
||||
url_prefixes = [
|
||||
_parse_url_prefix(p) for p in conf.get("fileserver.url_prefixes", [])
|
||||
]
|
||||
if not any(self.host == host for host, _ in url_prefixes):
|
||||
url_prefixes.append((self.host, ""))
|
||||
self.url_prefixes = url_prefixes
|
||||
|
||||
self.company = company
|
||||
|
||||
# @classmethod
|
||||
# def validate_fileserver_access(cls, fileserver_host: str):
|
||||
# res = requests.get(
|
||||
# url=fileserver_host
|
||||
# )
|
||||
# res.raise_for_status()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Fileserver"
|
||||
|
||||
def _resolve_base_url(self, url: UrlToDelete) -> Optional[str]:
|
||||
"""
|
||||
For the url return the base_url containing schema, optional host and bucket name
|
||||
"""
|
||||
if not url.url:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = furl(url.url)
|
||||
url_host = f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme else None
|
||||
url_path = str(parsed.path)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for host, path_prefix in self.url_prefixes:
|
||||
if host and url_host != host:
|
||||
continue
|
||||
if path_prefix and not url_path.startswith(path_prefix + "/"):
|
||||
continue
|
||||
return url_path[len(path_prefix or ""):]
|
||||
url.url = url_path[len(path_prefix or "") :]
|
||||
return self.host
|
||||
|
||||
raise ValueError("could not map path")
|
||||
def group_urls(
|
||||
self, urls: Sequence[UrlToDelete]
|
||||
) -> Mapping[str, Sequence[UrlToDelete]]:
|
||||
return bucketize(urls, key=self._resolve_base_url)
|
||||
|
||||
paths = set()
|
||||
path_to_id_mapping = defaultdict(list)
|
||||
for url in to_delete:
|
||||
try:
|
||||
path = resolve_path(url)
|
||||
path = path.strip("/")
|
||||
if not path:
|
||||
raise ValueError("Empty path")
|
||||
except Exception as ex:
|
||||
err = str(ex)
|
||||
log.warn(f"Error getting path for {url.url}: {err}")
|
||||
mark_failed(Q(id=url.id), err)
|
||||
continue
|
||||
|
||||
paths.add(path)
|
||||
path_to_id_mapping[path].append(url.id)
|
||||
|
||||
if not paths:
|
||||
return
|
||||
|
||||
ids_to_delete = set(chain.from_iterable(path_to_id_mapping.values()))
|
||||
try:
|
||||
res = requests.post(
|
||||
url=furl(fileserver_host).add(path="delete_many").url,
|
||||
json={"files": list(paths)},
|
||||
timeout=fileserver_timeout,
|
||||
)
|
||||
def get_client(self, base: str, urls: Sequence[UrlToDelete]) -> Client:
|
||||
host = base
|
||||
session = requests.session()
|
||||
res = session.get(url=host, timeout=self.Client.timeout)
|
||||
res.raise_for_status()
|
||||
except Exception as ex:
|
||||
err = str(ex)
|
||||
log.warn(f"Error deleting {len(paths)} files from fileserver: {err}")
|
||||
mark_retry_failed(list(ids_to_delete), err)
|
||||
return
|
||||
|
||||
res_data = res.json()
|
||||
deleted_ids = set(
|
||||
chain.from_iterable(
|
||||
path_to_id_mapping.get(path, [])
|
||||
for path in list(res_data.get("deleted", {}))
|
||||
return self.Client(session, host)
|
||||
|
||||
|
||||
class AzureStorage(Storage):
|
||||
class Client(Storage.Client):
|
||||
def __init__(self, container: ContainerClient):
|
||||
self.container = container
|
||||
|
||||
@property
|
||||
def chunk_size(self) -> int:
|
||||
return 256
|
||||
|
||||
def get_path(self, url: UrlToDelete) -> str:
|
||||
parsed = furl(url.url)
|
||||
if (
|
||||
not parsed.path
|
||||
or not parsed.path.segments
|
||||
or len(parsed.path.segments) <= 1
|
||||
):
|
||||
raise ValueError("No path found following container name")
|
||||
|
||||
return os.path.join(*parsed.path.segments[1:])
|
||||
|
||||
@staticmethod
|
||||
def _path_from_request_url(request_url: str) -> str:
|
||||
try:
|
||||
return furl(request_url).path.segments[-1]
|
||||
except Exception:
|
||||
return request_url
|
||||
|
||||
def delete_many(
|
||||
self, paths: Sequence[str]
|
||||
) -> Tuple[Sequence[str], Mapping[str, Sequence[str]]]:
|
||||
try:
|
||||
res = self.container.delete_blobs(*paths)
|
||||
except PartialBatchErrorException as pex:
|
||||
deleted = []
|
||||
errors = defaultdict(list)
|
||||
for part in pex.parts:
|
||||
if 300 >= part.status_code >= 200:
|
||||
deleted.append(self._path_from_request_url(part.request.url))
|
||||
else:
|
||||
errors[part.reason].append(
|
||||
self._path_from_request_url(part.request.url)
|
||||
)
|
||||
return deleted, errors
|
||||
|
||||
return [self._path_from_request_url(part.request.url) for part in res], {}
|
||||
|
||||
def __init__(self, company: str):
|
||||
self.configs = storage_bll.get_azure_settings_for_company(company)
|
||||
self.scheme = "azure"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Azure"
|
||||
|
||||
def _resolve_base_url(self, url: UrlToDelete) -> Optional[Tuple]:
|
||||
"""
|
||||
For the url return the base_url containing schema, optional host and bucket name
|
||||
"""
|
||||
try:
|
||||
parsed = urlparse(url.url)
|
||||
if parsed.scheme != self.scheme:
|
||||
return None
|
||||
|
||||
azure_conf = self.configs.get_config_by_uri(url.url)
|
||||
if azure_conf is None:
|
||||
return None
|
||||
|
||||
account_url = parsed.netloc
|
||||
return account_url, azure_conf.container_name
|
||||
except Exception as ex:
|
||||
log.warning(f"Error resolving base url for {url.url}: " + str(ex))
|
||||
return None
|
||||
|
||||
def group_urls(
|
||||
self, urls: Sequence[UrlToDelete]
|
||||
) -> Mapping[Tuple, Sequence[UrlToDelete]]:
|
||||
return bucketize(urls, key=self._resolve_base_url)
|
||||
|
||||
def get_client(self, base: Tuple, urls: Sequence[UrlToDelete]) -> Client:
|
||||
account_url, container_name = base
|
||||
sample_url = urls[0].url
|
||||
cfg = self.configs.get_config_by_uri(sample_url)
|
||||
if not cfg or not cfg.account_name or not cfg.account_key:
|
||||
raise ValueError(
|
||||
f"Missing account name or key for Azure Blob Storage "
|
||||
f"account: {account_url}, container: {container_name}"
|
||||
)
|
||||
|
||||
return self.Client(
|
||||
ContainerClient(
|
||||
account_url=account_url,
|
||||
container_name=cfg.container_name,
|
||||
credential={
|
||||
"account_name": cfg.account_name,
|
||||
"account_key": cfg.account_key,
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
if deleted_ids:
|
||||
UrlToDelete.objects(id__in=list(deleted_ids)).delete()
|
||||
log.info(f"{len(deleted_ids)} files deleted from the fileserver")
|
||||
|
||||
failed_ids = set()
|
||||
for err, error_ids in res_data.get("errors", {}).items():
|
||||
error_ids = list(
|
||||
chain.from_iterable(path_to_id_mapping.get(path, []) for path in error_ids)
|
||||
|
||||
class AWSStorage(Storage):
|
||||
class Client(Storage.Client):
|
||||
def __init__(self, base_url: str, container: AWSBucket):
|
||||
self.container = container
|
||||
self.base_url = base_url
|
||||
|
||||
@property
|
||||
def chunk_size(self) -> int:
|
||||
return 1000
|
||||
|
||||
def get_path(self, url: UrlToDelete) -> str:
|
||||
""" Normalize remote path. Remove any prefix that is already handled by the container """
|
||||
path = url.url
|
||||
if path.startswith(self.base_url):
|
||||
path = path[len(self.base_url) :]
|
||||
path = path.lstrip("/")
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _path_from_request_url(request_url: str) -> str:
|
||||
try:
|
||||
return furl(request_url).path.segments[-1]
|
||||
except Exception:
|
||||
return request_url
|
||||
|
||||
def delete_many(
|
||||
self, paths: Sequence[str]
|
||||
) -> Tuple[Sequence[str], Mapping[str, Sequence[str]]]:
|
||||
res = self.container.delete_objects(
|
||||
Delete={"Objects": [{"Key": p} for p in paths]}
|
||||
)
|
||||
errors = defaultdict(list)
|
||||
for err in res.get("Errors", []):
|
||||
msg = err.get("Message", "")
|
||||
errors[msg].append(err.get("Key"))
|
||||
|
||||
return [d.get("Key") for d in res.get("Deleted", [])], errors
|
||||
|
||||
def __init__(self, company: str):
|
||||
self.configs = storage_bll.get_aws_settings_for_company(company)
|
||||
self.scheme = "s3"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "AWS"
|
||||
|
||||
def _resolve_base_url(self, url: UrlToDelete) -> Optional[str]:
|
||||
"""
|
||||
For the url return the base_url containing schema, optional host and bucket name
|
||||
"""
|
||||
try:
|
||||
parsed = urlparse(url.url)
|
||||
if parsed.scheme != self.scheme:
|
||||
return None
|
||||
|
||||
s3_conf = self.configs.get_config_by_uri(url.url)
|
||||
if s3_conf is None:
|
||||
return None
|
||||
|
||||
s3_bucket = s3_conf.bucket
|
||||
if not s3_bucket:
|
||||
parts = Path(parsed.path.strip("/")).parts
|
||||
if parts:
|
||||
s3_bucket = parts[0]
|
||||
return "/".join(filter(None, ("s3:/", s3_conf.host, s3_bucket)))
|
||||
except Exception as ex:
|
||||
log.warning(f"Error resolving base url for {url.url}: " + str(ex))
|
||||
return None
|
||||
|
||||
def group_urls(
|
||||
self, urls: Sequence[UrlToDelete]
|
||||
) -> Mapping[str, Sequence[UrlToDelete]]:
|
||||
return bucketize(urls, key=self._resolve_base_url)
|
||||
|
||||
def get_client(self, base: str, urls: Sequence[UrlToDelete]) -> Client:
|
||||
sample_url = urls[0].url
|
||||
cfg = self.configs.get_config_by_uri(sample_url)
|
||||
boto_kwargs = {
|
||||
"endpoint_url": (("https://" if cfg.secure else "http://") + cfg.host)
|
||||
if cfg.host
|
||||
else None,
|
||||
"use_ssl": cfg.secure,
|
||||
"verify": cfg.verify,
|
||||
}
|
||||
name = base[len(scheme_prefix(self.scheme)) :]
|
||||
bucket_name = name[len(cfg.host) + 1 :] if cfg.host else name
|
||||
if not cfg.use_credentials_chain:
|
||||
if not cfg.key or not cfg.secret:
|
||||
raise ValueError(
|
||||
f"Missing key or secret for AWS S3 host: {cfg.host}, bucket: {str(bucket_name)}"
|
||||
)
|
||||
|
||||
boto_kwargs["aws_access_key_id"] = cfg.key
|
||||
boto_kwargs["aws_secret_access_key"] = cfg.secret
|
||||
if cfg.token:
|
||||
boto_kwargs["aws_session_token"] = cfg.token
|
||||
|
||||
return self.Client(
|
||||
base, boto3.resource("s3", **boto_kwargs).Bucket(bucket_name)
|
||||
)
|
||||
mark_retry_failed(error_ids, err)
|
||||
log.warning(
|
||||
f"Failed to delete {len(error_ids)} files from the fileserver due to: {err}"
|
||||
|
||||
|
||||
class GoogleCloudStorage(Storage):
|
||||
class Client(Storage.Client):
|
||||
def __init__(self, base_url: str, container: google_storage.Bucket):
|
||||
self.container = container
|
||||
self.base_url = base_url
|
||||
|
||||
@property
|
||||
def chunk_size(self) -> int:
|
||||
return 100
|
||||
|
||||
def get_path(self, url: UrlToDelete) -> str:
|
||||
""" Normalize remote path. Remove any prefix that is already handled by the container """
|
||||
path = url.url
|
||||
if path.startswith(self.base_url):
|
||||
path = path[len(self.base_url) :]
|
||||
path = path.lstrip("/")
|
||||
return path
|
||||
|
||||
def delete_many(
|
||||
self, paths: Sequence[str]
|
||||
) -> Tuple[Sequence[str], Mapping[str, Sequence[str]]]:
|
||||
not_found = set()
|
||||
|
||||
def error_callback(blob: google_storage.Blob):
|
||||
not_found.add(blob.name)
|
||||
|
||||
self.container.delete_blobs(
|
||||
[self.container.blob(p) for p in paths], on_error=error_callback,
|
||||
)
|
||||
errors = {"Not found": list(not_found)} if not_found else {}
|
||||
return list(set(paths) - not_found), errors
|
||||
|
||||
def __init__(self, company: str):
|
||||
self.configs = storage_bll.get_gs_settings_for_company(company)
|
||||
self.scheme = "gs"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Google Storage"
|
||||
|
||||
def _resolve_base_url(self, url: UrlToDelete) -> Optional[str]:
|
||||
"""
|
||||
For the url return the base_url containing schema, optional host and bucket name
|
||||
"""
|
||||
try:
|
||||
parsed = urlparse(url.url)
|
||||
if parsed.scheme != self.scheme:
|
||||
return None
|
||||
|
||||
gs_conf = self.configs.get_config_by_uri(url.url)
|
||||
if gs_conf is None:
|
||||
return None
|
||||
|
||||
return str(furl(scheme=parsed.scheme, netloc=gs_conf.bucket))
|
||||
except Exception as ex:
|
||||
log.warning(f"Error resolving base url for {url.url}: " + str(ex))
|
||||
return None
|
||||
|
||||
def group_urls(
|
||||
self, urls: Sequence[UrlToDelete]
|
||||
) -> Mapping[str, Sequence[UrlToDelete]]:
|
||||
return bucketize(urls, key=self._resolve_base_url)
|
||||
|
||||
def get_client(self, base: str, urls: Sequence[UrlToDelete]) -> Client:
|
||||
sample_url = urls[0].url
|
||||
cfg = self.configs.get_config_by_uri(sample_url)
|
||||
if cfg.credentials_json:
|
||||
from google.oauth2 import service_account
|
||||
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
cfg.credentials_json
|
||||
)
|
||||
else:
|
||||
credentials = None
|
||||
|
||||
bucket_name = base[len(scheme_prefix(self.scheme)) :]
|
||||
return self.Client(
|
||||
base,
|
||||
google_storage.Client(project=cfg.project, credentials=credentials).bucket(
|
||||
bucket_name
|
||||
),
|
||||
)
|
||||
failed_ids.update(error_ids)
|
||||
|
||||
missing_ids = ids_to_delete - deleted_ids - failed_ids
|
||||
if missing_ids:
|
||||
mark_retry_failed(list(missing_ids), "Not succeeded")
|
||||
|
||||
|
||||
def _get_fileserver_url_prefixes(fileserver_host: str) -> Sequence[UrlPrefix]:
|
||||
def _parse_url_prefix(prefix) -> UrlPrefix:
|
||||
url = furl(prefix)
|
||||
host = f"{url.scheme}://{url.netloc}" if url.scheme else None
|
||||
return host, str(url.path).rstrip("/")
|
||||
|
||||
url_prefixes = [
|
||||
_parse_url_prefix(p) for p in conf.get("fileserver.url_prefixes", [])
|
||||
]
|
||||
if not any(fileserver_host == host for host, _ in url_prefixes):
|
||||
url_prefixes.append((fileserver_host, ""))
|
||||
|
||||
return url_prefixes
|
||||
|
||||
|
||||
def run_delete_loop(fileserver_host: str):
|
||||
fileserver_host = validate_fileserver_access(fileserver_host)
|
||||
|
||||
storage_delete_funcs = {
|
||||
storage_helpers = {
|
||||
StorageType.fileserver: partial(
|
||||
delete_fileserver_urls,
|
||||
fileserver_host=fileserver_host,
|
||||
url_prefixes=_get_fileserver_url_prefixes(fileserver_host),
|
||||
FileserverStorage, fileserver_host=fileserver_host
|
||||
),
|
||||
StorageType.s3: AWSStorage,
|
||||
StorageType.azure: AzureStorage,
|
||||
StorageType.gs: GoogleCloudStorage,
|
||||
}
|
||||
while True:
|
||||
now = datetime.utcnow()
|
||||
@@ -177,7 +574,7 @@ def run_delete_loop(fileserver_host: str):
|
||||
)
|
||||
|
||||
url_to_delete: UrlToDelete = UrlToDelete.objects(
|
||||
urls_query & Q(storage_type__in=list(storage_delete_funcs))
|
||||
urls_query & Q(storage_type__in=list(storage_helpers))
|
||||
).order_by("retry_count").limit(1).first()
|
||||
if not url_to_delete:
|
||||
sleep(10)
|
||||
@@ -192,7 +589,10 @@ def run_delete_loop(fileserver_host: str):
|
||||
company_storage_urls_query = urls_query & Q(
|
||||
company=company, storage_type=storage_type,
|
||||
)
|
||||
storage_delete_funcs[storage_type](urls_query=company_storage_urls_query)
|
||||
delete_urls(
|
||||
urls_query=company_storage_urls_query,
|
||||
storage=storage_helpers[storage_type](company=company),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -18,9 +18,15 @@ _migration_dir = _parent_dir / _migrations
|
||||
|
||||
|
||||
def check_mongo_empty() -> bool:
|
||||
return not all(
|
||||
get_db(alias).collection_names() for alias in utils.get_options(Database)
|
||||
)
|
||||
for alias in utils.get_options(Database):
|
||||
collection_names = get_db(alias).list_collection_names()
|
||||
if collection_names and any(
|
||||
name in collection_names
|
||||
for name in ["company", "user", "versions"]
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_last_server_version() -> Version:
|
||||
|
||||
@@ -960,13 +960,17 @@ class PrePopulate:
|
||||
return tasks
|
||||
|
||||
@classmethod
|
||||
def _import_events(cls, f: IO[bytes], company_id: str, _, task_id: str):
|
||||
def _import_events(cls, f: IO[bytes], company_id: str, user_id: str, task_id: str):
|
||||
print(f"Writing events for task {task_id} into database")
|
||||
for events_chunk in chunked_iter(cls.json_lines(f), 1000):
|
||||
events = [json.loads(item) for item in events_chunk]
|
||||
for ev in events:
|
||||
ev["task"] = task_id
|
||||
ev["company_id"] = company_id
|
||||
ev["allow_locked"] = True
|
||||
cls.event_bll.add_events(
|
||||
company_id, events=events, worker="", allow_locked=True
|
||||
company_id=company_id,
|
||||
user_id=user_id,
|
||||
events=events,
|
||||
worker="",
|
||||
)
|
||||
|
||||
17
apiserver/mongo/migrations/1_9_0.py
Normal file
17
apiserver/mongo/migrations/1_9_0.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import logging as log
|
||||
|
||||
from pymongo.collection import Collection
|
||||
from pymongo.database import Database
|
||||
from pymongo.errors import OperationFailure
|
||||
|
||||
|
||||
def migrate_backend(db: Database):
|
||||
"""
|
||||
Drop task text index so that the new one including reports field is created
|
||||
"""
|
||||
tasks: Collection = db["task"]
|
||||
try:
|
||||
tasks.drop_index("backend-db.task.main_text_index")
|
||||
except OperationFailure as ex:
|
||||
log.warning(f"Could not delete task text index due to: {str(ex)}")
|
||||
pass
|
||||
@@ -2,7 +2,7 @@ from os import getenv
|
||||
|
||||
from boltons.iterutils import first
|
||||
from redis import StrictRedis
|
||||
from rediscluster import RedisCluster
|
||||
from redis.cluster import RedisCluster
|
||||
|
||||
from apiserver.apierrors.errors.server_error import ConfigError, GeneralError
|
||||
from apiserver.config_repo import config
|
||||
@@ -83,7 +83,7 @@ class RedisManager(object):
|
||||
def host(self, alias):
|
||||
r = self.connection(alias)
|
||||
if isinstance(r, RedisCluster):
|
||||
connections = first(r.connection_pool._available_connections.values())
|
||||
connections = r.get_default_node().redis_connection.connection_pool._available_connections
|
||||
else:
|
||||
connections = r.connection_pool._available_connections
|
||||
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
attrs>=22.1.0
|
||||
attrs>=22.1.0,<23
|
||||
azure-storage-blob>=12.13.1
|
||||
bcrypt>=3.1.4
|
||||
boltons>=19.1.0
|
||||
boto3==1.14.13
|
||||
boto3>=1.26
|
||||
boto3-stubs[s3]>=1.26
|
||||
clearml>=1.10.3
|
||||
dpath>=1.4.2,<2.0
|
||||
elasticsearch==7.13.3
|
||||
elasticsearch==7.17.9
|
||||
fastjsonschema>=2.8
|
||||
flask-compress>=1.4.0
|
||||
flask-cors>=3.0.5
|
||||
flask>=0.12.2
|
||||
funcsigs==1.0.2
|
||||
flask>=2.3.2
|
||||
furl>=2.0.0
|
||||
gunicorn>=19.7.1
|
||||
humanfriendly==4.18
|
||||
jinja2==2.11.3
|
||||
google-cloud-storage>=2.8.0
|
||||
gunicorn>=20.1.0
|
||||
humanfriendly>=4.17
|
||||
jinja2
|
||||
jsonmodels>=2.3
|
||||
jsonschema>=2.6.0
|
||||
luqum>=0.10.0
|
||||
mongoengine==0.23.1
|
||||
mongoengine==0.27.0
|
||||
nested_dict>=1.61
|
||||
packaging==20.3
|
||||
psutil>=5.6.5
|
||||
pyhocon>=0.3.35
|
||||
pyhocon>=0.3.35r
|
||||
pyjwt>=2.4.0
|
||||
pymongo[srv]==3.12.0
|
||||
pymongo==4.4.0
|
||||
python-rapidjson>=0.6.3
|
||||
redis==3.5.3
|
||||
redis-py-cluster>=2.1.3
|
||||
redis>=4.5.4,<5
|
||||
requests>=2.13.0
|
||||
semantic_version>=2.8.3,<3
|
||||
setuptools>=65.5.1
|
||||
six
|
||||
tqdm
|
||||
validators>=0.12.4
|
||||
validators>=0.12.4
|
||||
urllib3>=1.26.18
|
||||
|
||||
@@ -1,3 +1,43 @@
|
||||
field_filter {
|
||||
type: object
|
||||
description: Filter on a field that includes combination of 'any' or 'all' included and excluded terms
|
||||
properties {
|
||||
any {
|
||||
type: object
|
||||
description: All the terms in 'any' condition are combined with 'or' operation
|
||||
properties {
|
||||
"include" {
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
exclude {
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
all {
|
||||
type: object
|
||||
description: All the terms in 'all' condition are combined with 'and' operation
|
||||
properties {
|
||||
"include" {
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
exclude {
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
op {
|
||||
type: string
|
||||
description: The operation between 'any' and 'all' parts of the filter if both are provided
|
||||
default: and
|
||||
enum: [and, or]
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata_item {
|
||||
type: object
|
||||
properties {
|
||||
@@ -15,6 +55,35 @@ metadata_item {
|
||||
}
|
||||
}
|
||||
}
|
||||
task_status_enum {
|
||||
type: string
|
||||
enum: [
|
||||
created
|
||||
queued
|
||||
in_progress
|
||||
stopped
|
||||
published
|
||||
publishing
|
||||
closed
|
||||
failed
|
||||
completed
|
||||
unknown
|
||||
]
|
||||
}
|
||||
multi_field_pattern_data {
|
||||
type: object
|
||||
properties {
|
||||
pattern {
|
||||
description: "Pattern string (regex)"
|
||||
type: string
|
||||
}
|
||||
fields {
|
||||
description: "List of field names"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
credentials {
|
||||
type: object
|
||||
properties {
|
||||
|
||||
141
apiserver/schema/services/_events_common.conf
Normal file
141
apiserver/schema/services/_events_common.conf
Normal file
@@ -0,0 +1,141 @@
|
||||
scalar_key_enum {
|
||||
type: string
|
||||
enum: [
|
||||
iter
|
||||
timestamp
|
||||
iso_time
|
||||
]
|
||||
}
|
||||
metric_variants {
|
||||
type: object
|
||||
properties {
|
||||
metric {
|
||||
description: The metric name
|
||||
type: string
|
||||
}
|
||||
variants {
|
||||
type: array
|
||||
description: The names of the metric variants
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_images_response_task_metrics {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
type: string
|
||||
description: Task ID
|
||||
}
|
||||
iterations {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
iter {
|
||||
type: integer
|
||||
description: Iteration number
|
||||
}
|
||||
events {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
description: Debug image event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_images_response {
|
||||
type: object
|
||||
properties {
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID for getting more results"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: "Debug image events grouped by tasks and iterations"
|
||||
items {"$ref": "#/definitions/debug_images_response_task_metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
plots_response_task_metrics {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
type: string
|
||||
description: Task ID
|
||||
}
|
||||
iterations {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
iter {
|
||||
type: integer
|
||||
description: Iteration number
|
||||
}
|
||||
events {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
description: Plot event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plots_response {
|
||||
type: object
|
||||
properties {
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID for getting more results"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: "Plot events grouped by tasks and iterations"
|
||||
items {"$ref": "#/definitions/plots_response_task_metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
single_value_task_metrics {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
type: string
|
||||
description: Task ID
|
||||
}
|
||||
task_name {
|
||||
type: string
|
||||
description: Task name
|
||||
}
|
||||
values {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
metric { type: string }
|
||||
variant { type: string}
|
||||
value { type: number }
|
||||
timestamp { type: number }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
single_value_metrics_response {
|
||||
type: object
|
||||
properties {
|
||||
tasks {
|
||||
description: Single value metrics grouped by task
|
||||
type: array
|
||||
items {"$ref": "#/definitions/single_value_task_metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
506
apiserver/schema/services/_tasks_common.conf
Normal file
506
apiserver/schema/services/_tasks_common.conf
Normal file
@@ -0,0 +1,506 @@
|
||||
include "_common.conf"
|
||||
task_type_enum {
|
||||
type: string
|
||||
enum: [
|
||||
training
|
||||
testing
|
||||
inference
|
||||
data_processing
|
||||
application
|
||||
monitor
|
||||
controller
|
||||
optimizer
|
||||
service
|
||||
qc
|
||||
custom
|
||||
]
|
||||
}
|
||||
script {
|
||||
type: object
|
||||
properties {
|
||||
binary {
|
||||
description: "Binary to use when running the script"
|
||||
type: string
|
||||
default: python
|
||||
}
|
||||
repository {
|
||||
description: "Name of the repository where the script is located"
|
||||
type: string
|
||||
}
|
||||
tag {
|
||||
description: "Repository tag"
|
||||
type: string
|
||||
}
|
||||
branch {
|
||||
description: "Repository branch id If not provided and tag not provided, default repository branch is used."
|
||||
type: string
|
||||
}
|
||||
version_num {
|
||||
description: "Version (changeset) number. Optional (default is head version) Unused if tag is provided."
|
||||
type: string
|
||||
}
|
||||
entry_point {
|
||||
description: "Path to execute within the repository"
|
||||
type: string
|
||||
}
|
||||
working_dir {
|
||||
description: "Path to the folder from which to run the script Default - root folder of repository"
|
||||
type: string
|
||||
}
|
||||
requirements {
|
||||
description: "A JSON object containing requirements strings by key"
|
||||
type: object
|
||||
}
|
||||
diff {
|
||||
description: "Uncommitted changes found in the repository when task was run"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
model_type_enum {
|
||||
type: string
|
||||
enum: ["input", "output"]
|
||||
}
|
||||
task_model_item {
|
||||
type: object
|
||||
required: [ name, model]
|
||||
properties {
|
||||
name {
|
||||
description: "The task model name"
|
||||
type: string
|
||||
}
|
||||
model {
|
||||
description: "The model ID"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
output {
|
||||
type: object
|
||||
properties {
|
||||
destination {
|
||||
description: "Storage id. This is where output files will be stored."
|
||||
type: string
|
||||
}
|
||||
model {
|
||||
description: "Model id."
|
||||
type: string
|
||||
}
|
||||
result {
|
||||
description: "Task result. Values: 'success', 'failure'"
|
||||
type: string
|
||||
}
|
||||
error {
|
||||
description: "Last error text"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
task_execution_progress_enum {
|
||||
type: string
|
||||
enum: [
|
||||
unknown
|
||||
running
|
||||
stopping
|
||||
stopped
|
||||
]
|
||||
}
|
||||
artifact_type_data {
|
||||
type: object
|
||||
properties {
|
||||
preview {
|
||||
description: "Description or textual data"
|
||||
type: string
|
||||
}
|
||||
content_type {
|
||||
description: "System defined raw data content type"
|
||||
type: string
|
||||
}
|
||||
data_hash {
|
||||
description: "Hash of raw data, without any headers or descriptive parts"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
artifact_mode_enum {
|
||||
type: string
|
||||
enum: [
|
||||
input
|
||||
output
|
||||
]
|
||||
default: output
|
||||
}
|
||||
artifact {
|
||||
type: object
|
||||
required: [key, type]
|
||||
properties {
|
||||
key {
|
||||
description: "Entry key"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "System defined type"
|
||||
type: string
|
||||
}
|
||||
mode {
|
||||
description: "System defined input/output indication"
|
||||
"$ref": "#/definitions/artifact_mode_enum"
|
||||
}
|
||||
uri {
|
||||
description: "Raw data location"
|
||||
type: string
|
||||
}
|
||||
content_size {
|
||||
description: "Raw data length in bytes"
|
||||
type: integer
|
||||
}
|
||||
hash {
|
||||
description: "Hash of entire raw data"
|
||||
type: string
|
||||
}
|
||||
timestamp {
|
||||
description: "Epoch time when artifact was created"
|
||||
type: integer
|
||||
}
|
||||
type_data {
|
||||
description: "Additional fields defined by the system"
|
||||
"$ref": "#/definitions/artifact_type_data"
|
||||
}
|
||||
display_data {
|
||||
description: "User-defined list of key/value pairs, sorted"
|
||||
type: array
|
||||
items {
|
||||
type: array
|
||||
items {
|
||||
type: string # can also be a number... TODO: upgrade the generator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
artifact_id {
|
||||
type: object
|
||||
required: [key]
|
||||
properties {
|
||||
key {
|
||||
description: "Entry key"
|
||||
type: string
|
||||
}
|
||||
mode {
|
||||
description: "System defined input/output indication"
|
||||
"$ref": "#/definitions/artifact_mode_enum"
|
||||
}
|
||||
}
|
||||
}
|
||||
task_models {
|
||||
type: object
|
||||
properties {
|
||||
input {
|
||||
description: "The list of task input models"
|
||||
type: array
|
||||
items {"$ref": "#/definitions/task_model_item"}
|
||||
|
||||
}
|
||||
output {
|
||||
description: "The list of task output models"
|
||||
type: array
|
||||
items {"$ref": "#/definitions/task_model_item"}
|
||||
}
|
||||
}
|
||||
}
|
||||
execution {
|
||||
type: object
|
||||
properties {
|
||||
queue {
|
||||
description: "Queue ID where task was queued."
|
||||
type: string
|
||||
}
|
||||
parameters {
|
||||
description: "Json object containing the Task parameters"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
model {
|
||||
description: "Execution input model ID Not applicable for Register (Import) tasks"
|
||||
type: string
|
||||
}
|
||||
model_desc {
|
||||
description: "Json object representing the Model descriptors"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
model_labels {
|
||||
description: """Json object representing the ids of the labels in the model.
|
||||
The keys are the layers' names and the values are the IDs.
|
||||
Not applicable for Register (Import) tasks.
|
||||
Mandatory for Training tasks"""
|
||||
type: object
|
||||
additionalProperties: { type: integer }
|
||||
}
|
||||
framework {
|
||||
description: """Framework related to the task. Case insensitive. Mandatory for Training tasks. """
|
||||
type: string
|
||||
}
|
||||
docker_cmd {
|
||||
description: "Command for running docker script for the execution of the task"
|
||||
type: string
|
||||
}
|
||||
artifacts {
|
||||
description: "Task artifacts"
|
||||
type: array
|
||||
items { "$ref": "#/definitions/artifact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
last_metrics_event {
|
||||
type: object
|
||||
properties {
|
||||
metric {
|
||||
description: "Metric name"
|
||||
type: string
|
||||
}
|
||||
variant {
|
||||
description: "Variant name"
|
||||
type: string
|
||||
}
|
||||
value {
|
||||
description: "Last value reported"
|
||||
type: number
|
||||
}
|
||||
min_value {
|
||||
description: "Minimum value reported"
|
||||
type: number
|
||||
}
|
||||
min_value_iteration {
|
||||
description: "The iteration at which the minimum value was reported"
|
||||
type: integer
|
||||
}
|
||||
max_value {
|
||||
description: "Maximum value reported"
|
||||
type: number
|
||||
}
|
||||
max_value_iteration {
|
||||
description: "The iteration at which the maximum value was reported"
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
last_metrics_variants {
|
||||
type: object
|
||||
description: "Last metric events, one for each variant hash"
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/last_metrics_event"
|
||||
}
|
||||
}
|
||||
params_item {
|
||||
type: object
|
||||
properties {
|
||||
section {
|
||||
description: "Section that the parameter belongs to"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "Name of the parameter. The combination of section and name should be unique"
|
||||
type: string
|
||||
}
|
||||
value {
|
||||
description: "Value of the parameter"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "Type of the parameter. Optional"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "The parameter description. Optional"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
configuration_item {
|
||||
type: object
|
||||
properties {
|
||||
name {
|
||||
description: "Name of the parameter. Should be unique"
|
||||
type: string
|
||||
}
|
||||
value {
|
||||
description: "Value of the parameter"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "Type of the parameter. Optional"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "The parameter description. Optional"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
section_params {
|
||||
description: "Task section params"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/params_item"
|
||||
}
|
||||
}
|
||||
task {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "Task id"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "Task Name"
|
||||
type: string
|
||||
}
|
||||
user {
|
||||
description: "Associated user id"
|
||||
type: string
|
||||
}
|
||||
company {
|
||||
description: "Company ID"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "Type of task. Values: 'training', 'testing'"
|
||||
"$ref": "#/definitions/task_type_enum"
|
||||
}
|
||||
status {
|
||||
description: ""
|
||||
"$ref": "#/definitions/task_status_enum"
|
||||
}
|
||||
comment {
|
||||
description: "Free text comment"
|
||||
type: string
|
||||
}
|
||||
created {
|
||||
description: "Task creation time (UTC) "
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
started {
|
||||
description: "Task start time (UTC)"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
completed {
|
||||
description: "Task end time (UTC)"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
active_duration {
|
||||
description: "Task duration time (seconds)"
|
||||
type: integer
|
||||
}
|
||||
parent {
|
||||
description: "Parent task id"
|
||||
type: string
|
||||
}
|
||||
project {
|
||||
description: "Project ID of the project to which this task is assigned"
|
||||
type: string
|
||||
}
|
||||
output {
|
||||
description: "Task output params"
|
||||
"$ref": "#/definitions/output"
|
||||
}
|
||||
execution {
|
||||
description: "Task execution params"
|
||||
"$ref": "#/definitions/execution"
|
||||
}
|
||||
container {
|
||||
description: "Docker container parameters"
|
||||
type: object
|
||||
additionalProperties { type: string }
|
||||
}
|
||||
models {
|
||||
description: "Task models"
|
||||
"$ref": "#/definitions/task_models"
|
||||
}
|
||||
// TODO: will be removed
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags list"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
system_tags {
|
||||
description: "System tags list. This field is reserved for system use, please don't use it."
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
status_changed {
|
||||
description: "Last status change time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
status_message {
|
||||
description: "free text string representing info about the status"
|
||||
type: string
|
||||
}
|
||||
status_reason {
|
||||
description: "Reason for last status change"
|
||||
type: string
|
||||
}
|
||||
published {
|
||||
description: "Task publish time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_worker {
|
||||
description: "ID of last worker that handled the task"
|
||||
type: string
|
||||
}
|
||||
last_worker_report {
|
||||
description: "Last time a worker reported while working on this task"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_update {
|
||||
description: "Last time this task was created, edited, changed or events for this task were reported"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_change {
|
||||
description: "Last time any update was done to the task"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_iteration {
|
||||
description: "Last iteration reported for this task"
|
||||
type: integer
|
||||
}
|
||||
last_metrics {
|
||||
description: "Last metric variants (hash to events), one for each metric hash"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/last_metrics_variants"
|
||||
}
|
||||
}
|
||||
hyperparams {
|
||||
description: "Task hyper params per section"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/section_params"
|
||||
}
|
||||
}
|
||||
configuration {
|
||||
description: "Task configuration params"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/configuration_item"
|
||||
}
|
||||
}
|
||||
runtime {
|
||||
description: "Task runtime mapping"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,6 @@
|
||||
_description : "Provides an API for running tasks to report events collected by the system."
|
||||
_definitions {
|
||||
metric_variants {
|
||||
type: object
|
||||
metric {
|
||||
description: The metric name
|
||||
type: string
|
||||
}
|
||||
variants {
|
||||
type: array
|
||||
description: The names of the metric variants
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
include "_events_common.conf"
|
||||
metrics_scalar_event {
|
||||
description: "Used for reporting scalar metrics during training task"
|
||||
type: object
|
||||
@@ -22,8 +11,8 @@ _definitions {
|
||||
type: number
|
||||
}
|
||||
type {
|
||||
description: "training_stats_vector"
|
||||
const: "training_stats_scalar"
|
||||
description: "'training_stats_scalar'"
|
||||
type: string
|
||||
}
|
||||
task {
|
||||
description: "Task ID (required)"
|
||||
@@ -57,8 +46,8 @@ _definitions {
|
||||
type: number
|
||||
}
|
||||
type {
|
||||
description: "training_stats_vector"
|
||||
const: "training_stats_vector"
|
||||
description: "'training_stats_vector'"
|
||||
type: string
|
||||
}
|
||||
task {
|
||||
description: "Task ID (required)"
|
||||
@@ -93,8 +82,8 @@ _definitions {
|
||||
type: number
|
||||
}
|
||||
type {
|
||||
description: ""
|
||||
const: "training_debug_image"
|
||||
description: "'training_debug_image'"
|
||||
type: string
|
||||
}
|
||||
task {
|
||||
description: "Task ID (required)"
|
||||
@@ -134,7 +123,7 @@ _definitions {
|
||||
}
|
||||
type {
|
||||
description: "'plot'"
|
||||
const: "plot"
|
||||
type: string
|
||||
}
|
||||
task {
|
||||
description: "Task ID (required)"
|
||||
@@ -164,14 +153,6 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
scalar_key_enum {
|
||||
type: string
|
||||
enum: [
|
||||
iter
|
||||
timestamp
|
||||
iso_time
|
||||
]
|
||||
}
|
||||
log_level_enum {
|
||||
type: string
|
||||
enum: [
|
||||
@@ -240,7 +221,7 @@ _definitions {
|
||||
}
|
||||
type {
|
||||
description: "'log'"
|
||||
const: "log"
|
||||
type: string
|
||||
}
|
||||
task {
|
||||
description: "Task ID (required)"
|
||||
@@ -260,90 +241,6 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_images_response_task_metrics {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
type: string
|
||||
description: Task ID
|
||||
}
|
||||
iterations {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
iter {
|
||||
type: integer
|
||||
description: Iteration number
|
||||
}
|
||||
events {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
description: Debug image event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_images_response {
|
||||
type: object
|
||||
properties {
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID for getting more results"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: "Debug image events grouped by tasks and iterations"
|
||||
items {"$ref": "#/definitions/debug_images_response_task_metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
plots_response_task_metrics {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
type: string
|
||||
description: Task ID
|
||||
}
|
||||
iterations {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
iter {
|
||||
type: integer
|
||||
description: Iteration number
|
||||
}
|
||||
events {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
description: Plot event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plots_response {
|
||||
type: object
|
||||
properties {
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID for getting more results"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: "Plot events grouped by tasks and iterations"
|
||||
items {"$ref": "#/definitions/plots_response_task_metrics"}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_image_sample_response {
|
||||
type: object
|
||||
properties {
|
||||
@@ -547,7 +444,7 @@ debug_images {
|
||||
}
|
||||
total {
|
||||
type: number
|
||||
description: "Total number of results available for this query"
|
||||
description: "Total number of results available for this query. In case there are more than 10000 results it is set to 10000"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -601,7 +498,7 @@ debug_images {
|
||||
}
|
||||
}
|
||||
"2.22": ${debug_images."2.14"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -624,7 +521,7 @@ plots {
|
||||
}
|
||||
iters {
|
||||
type: integer
|
||||
description: "Max number of latest iterations for which to return debug images"
|
||||
description: "Max number of latest iterations for which to return plots"
|
||||
}
|
||||
navigate_earlier {
|
||||
type: boolean
|
||||
@@ -643,7 +540,7 @@ plots {
|
||||
response {"$ref": "#/definitions/plots_response"}
|
||||
}
|
||||
"2.22": ${plots."2.20"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model plots. Otherwise task plots
|
||||
default: false
|
||||
@@ -693,7 +590,7 @@ get_debug_image_sample {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_debug_image_sample."2.20"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model debug images. Otherwise task debug images
|
||||
default: false
|
||||
@@ -730,7 +627,7 @@ next_debug_image_sample {
|
||||
default: false
|
||||
description: If set then navigate to the next/previous iteration
|
||||
}
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model debug images. Otherwise task debug images
|
||||
default: false
|
||||
@@ -774,7 +671,7 @@ get_plot_sample {
|
||||
response {"$ref": "#/definitions/plot_sample_response"}
|
||||
}
|
||||
"2.22": ${get_plot_sample."2.20"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model plots. Otherwise task plots
|
||||
default: false
|
||||
@@ -811,7 +708,7 @@ next_plot_sample {
|
||||
default: false
|
||||
description: If set then navigate to the next/previous iteration
|
||||
}
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model plots. Otherwise task plots
|
||||
default: false
|
||||
@@ -850,7 +747,7 @@ get_task_metrics{
|
||||
}
|
||||
}
|
||||
"2.22": ${get_task_metrics."2.7"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then get metrics from model events. Otherwise from task events
|
||||
default: false
|
||||
@@ -955,7 +852,7 @@ get_task_log {
|
||||
}
|
||||
total {
|
||||
type: number
|
||||
description: "Total number of results available for this query"
|
||||
description: "Total number of results available for this query. In case there are more than 10000 results it is set to 10000"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -1009,7 +906,7 @@ get_task_log {
|
||||
}
|
||||
total {
|
||||
type: number
|
||||
description: "Total number of log events available for this query"
|
||||
description: "Total number of log events available for this query. In case there are more than 10000 events it is set to 10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1064,7 +961,7 @@ get_task_events {
|
||||
}
|
||||
total {
|
||||
type: number
|
||||
description: "Total number of results available for this query"
|
||||
description: "Total number of results available for this query. In case there are more than 10000 results it is set to 10000"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -1074,7 +971,7 @@ get_task_events {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_task_events."2.1"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then get retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1130,7 +1027,7 @@ get_task_plots {
|
||||
}
|
||||
iters {
|
||||
type: integer
|
||||
description: "Max number of latest iterations for which to return debug images"
|
||||
description: "Max number of latest iterations for which to return plots"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -1154,7 +1051,7 @@ get_task_plots {
|
||||
}
|
||||
total {
|
||||
type: number
|
||||
description: "Total number of results available for this query"
|
||||
description: "Total number of results available for this query. In case there are more than 10000 results it is set to 10000"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -1182,7 +1079,7 @@ get_task_plots {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_task_plots."2.16"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1208,7 +1105,7 @@ get_multi_task_plots {
|
||||
}
|
||||
iters {
|
||||
type: integer
|
||||
description: "Max number of latest iterations for which to return debug images"
|
||||
description: "Max number of latest iterations for which to return plots"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -1229,7 +1126,7 @@ get_multi_task_plots {
|
||||
}
|
||||
total {
|
||||
type: number
|
||||
description: "Total number of results available for this query"
|
||||
description: "Total number of results available for this query. In case there are more than 10000 results it is set to 10000"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
@@ -1246,12 +1143,19 @@ get_multi_task_plots {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_multi_task_plots."2.16"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.26": ${get_multi_task_plots."2.22"} {
|
||||
request.properties.last_iters_per_task_metric {
|
||||
type: boolean
|
||||
description: If set to 'true' and iters passed then last iterations for each task metrics are retrieved. Otherwise last iterations for the whole task are retrieved
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
get_vector_metrics_and_variants {
|
||||
"2.1" {
|
||||
@@ -1281,7 +1185,7 @@ get_vector_metrics_and_variants {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_vector_metrics_and_variants."2.1"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1326,7 +1230,7 @@ vector_metrics_iter_histogram {
|
||||
}
|
||||
}
|
||||
"2.22": ${vector_metrics_iter_histogram."2.1"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1386,7 +1290,7 @@ scalar_metrics_iter_histogram {
|
||||
}
|
||||
}
|
||||
"2.22": ${scalar_metrics_iter_histogram."2.14"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1403,7 +1307,7 @@ multi_task_scalar_metrics_iter_histogram {
|
||||
]
|
||||
properties {
|
||||
tasks {
|
||||
description: "List of task Task IDs. Maximum amount of tasks is 10"
|
||||
description: "List of task Task IDs. Maximum amount of tasks is 100"
|
||||
type: array
|
||||
items {
|
||||
type: string
|
||||
@@ -1432,7 +1336,7 @@ multi_task_scalar_metrics_iter_histogram {
|
||||
}
|
||||
}
|
||||
"2.22": ${multi_task_scalar_metrics_iter_histogram."2.1"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1456,39 +1360,10 @@ get_task_single_value_metrics {
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
tasks {
|
||||
description: Single value metrics grouped by task
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
type: string
|
||||
description: Task ID
|
||||
}
|
||||
values {
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
metric { type: string }
|
||||
variant { type: string}
|
||||
value { type: number }
|
||||
timestamp { type: number }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {"$ref": "#/definitions/single_value_metrics_response"}
|
||||
}
|
||||
"2.22": ${get_task_single_value_metrics."2.20"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1574,7 +1449,7 @@ get_scalar_metrics_and_variants {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_scalar_metrics_and_variants."2.1"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1595,6 +1470,10 @@ get_scalar_metric_data {
|
||||
type: string
|
||||
description: type of metric
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID of previous call (used for getting more results)"
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
@@ -1617,7 +1496,7 @@ get_scalar_metric_data {
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID of previous call (used for getting more results)"
|
||||
description: "Scroll ID for getting more results"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1630,7 +1509,7 @@ get_scalar_metric_data {
|
||||
}
|
||||
}
|
||||
"2.22": ${get_scalar_metric_data."2.16"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1701,7 +1580,7 @@ scalar_metrics_iter_raw {
|
||||
}
|
||||
}
|
||||
"2.22": ${scalar_metrics_iter_raw."2.16"} {
|
||||
model_events {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
@@ -1762,4 +1641,4 @@ clear_task_log {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ _default {
|
||||
}
|
||||
|
||||
supported_modes {
|
||||
authorize: false
|
||||
authorize: null
|
||||
"2.9" {
|
||||
description: """ Return supported login modes."""
|
||||
request {
|
||||
@@ -59,7 +59,7 @@ supported_modes {
|
||||
description: "SSO authentication providers"
|
||||
type: object
|
||||
additionalProperties {
|
||||
desctiprion: "Provider redirect URL"
|
||||
description: "Provider redirect URL"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ supported_modes {
|
||||
}
|
||||
|
||||
logout {
|
||||
authorize: false
|
||||
authorize: null
|
||||
allow_roles = [ "*" ]
|
||||
"2.13" {
|
||||
description: """ Logout (including SSO, if used)) """
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
_description: """This service provides a management interface for models (results of training tasks) stored in the system."""
|
||||
_definitions {
|
||||
include "_common.conf"
|
||||
include "_tasks_common.conf"
|
||||
multi_field_pattern_data {
|
||||
type: object
|
||||
properties {
|
||||
@@ -104,6 +104,17 @@ _definitions {
|
||||
"$ref": "#/definitions/metadata_item"
|
||||
}
|
||||
}
|
||||
last_iteration {
|
||||
description: "Last iteration reported for this model"
|
||||
type: integer
|
||||
}
|
||||
last_metrics {
|
||||
description: "Last metric variants (hash to events), one for each metric hash"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/last_metrics_variants"
|
||||
}
|
||||
}
|
||||
stats {
|
||||
description: "Model statistics"
|
||||
type: object
|
||||
@@ -241,6 +252,23 @@ get_all_ex {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.23": ${get_all_ex."2.20"} {
|
||||
request.properties {
|
||||
allow_public {
|
||||
description: "Allow public models to be returned in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.27": ${get_all_ex."2.23"} {
|
||||
request.properties {
|
||||
filters {
|
||||
type: object
|
||||
additionalProperties: ${_definitions.field_filter}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all {
|
||||
"2.1" {
|
||||
@@ -337,9 +365,6 @@ get_all {
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
page: [ page_size ]
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
@@ -375,6 +400,17 @@ get_all {
|
||||
description: "Scroll ID that can be used with the next calls to get_all to retrieve more data"
|
||||
}
|
||||
}
|
||||
"2.26": ${get_all."2.15"} {
|
||||
request {
|
||||
properties {
|
||||
include_subprojects {
|
||||
description: "If set to 'true' and project field is set then models from the subprojects are searched too"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_frameworks {
|
||||
"2.8" {
|
||||
@@ -976,7 +1012,7 @@ move {
|
||||
items { type: string }
|
||||
}
|
||||
project {
|
||||
description: "Target project ID. If not provided, `project_name` must be provided."
|
||||
description: "Target project ID. If not provided, `project_name` must be provided. Use null for the root project"
|
||||
type: string
|
||||
}
|
||||
project_name {
|
||||
@@ -1055,4 +1091,38 @@ delete_metadata {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
update_tags {
|
||||
"2.27" {
|
||||
description: Add or remove tags from multiple models
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
ids {
|
||||
type: array
|
||||
description: IDs of the models to update
|
||||
items {type: string}
|
||||
}
|
||||
add_tags {
|
||||
type: array
|
||||
description: User tags to add
|
||||
items {type: string}
|
||||
}
|
||||
remove_tags {
|
||||
type: array
|
||||
description: User tags to remove
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
updated {
|
||||
type: integer
|
||||
description: The number of updated models
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,38 @@
|
||||
_description: "This service provides organization level operations"
|
||||
|
||||
_definitions {
|
||||
value_mapping {
|
||||
type: object
|
||||
required: [key, value]
|
||||
properties {
|
||||
key {
|
||||
description: Original value
|
||||
type: object
|
||||
}
|
||||
value {
|
||||
description: Translated value
|
||||
type: object
|
||||
}
|
||||
}
|
||||
}
|
||||
field_mapping {
|
||||
type: object
|
||||
required: [field]
|
||||
properties {
|
||||
field {
|
||||
description: The source field name as specified in the only_fields
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: The column name in the exported csv file
|
||||
type: string
|
||||
}
|
||||
values {
|
||||
type: array
|
||||
items { "$ref": "#/definitions/value_mapping"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_tags {
|
||||
"2.8" {
|
||||
description: "Get all the user and system tags used for the company tasks and models"
|
||||
@@ -170,10 +203,96 @@ get_entities_count {
|
||||
default: false
|
||||
}
|
||||
active_users {
|
||||
descritpion: "The list of users that were active in the project. If passes then the resulting projects are filtered to the ones that have tasks created by these users"
|
||||
description: "The list of users that were active in the project. If passes then the resulting projects are filtered to the ones that have tasks created by these users"
|
||||
type: array
|
||||
items: {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.23": ${get_entities_count."2.22"} {
|
||||
request.properties {
|
||||
reports {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Search criteria for reports
|
||||
}
|
||||
allow_public {
|
||||
description: "Allow public entities to be counted in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
response.properties {
|
||||
reports {
|
||||
type: integer
|
||||
description: The number of reports matching the criteria
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prepare_download_for_get_all {
|
||||
"2.26": {
|
||||
description: Prepares download from get_all_ex parameters
|
||||
request {
|
||||
type: object
|
||||
required: [ entity_type, only_fields, field_mappings]
|
||||
properties {
|
||||
only_fields {
|
||||
description: "List of task field names (nesting is supported using '.', e.g. execution.model_labels). If provided, this list defines the query's projection (only these fields will be returned for each result entry)"
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
allow_public {
|
||||
description: "Allow public entities to be returned in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
search_hidden {
|
||||
description: "If set to 'true' then hidden entities are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
entity_type {
|
||||
description: "The type of the entity to retrieve"
|
||||
type: string
|
||||
enum: [
|
||||
task
|
||||
model
|
||||
]
|
||||
}
|
||||
field_mappings {
|
||||
description: The name and value mappings for the exported fields. The fields that are not in the mappings will not be exported
|
||||
type: array
|
||||
items { "$ref": "#/definitions/field_mapping"}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
prepare_id {
|
||||
description: "Prepare ID (use when calling 'download_for_get_all')"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
download_for_get_all {
|
||||
"2.26": {
|
||||
description: Generates a file for the download
|
||||
request {
|
||||
type: object
|
||||
required: [ prepare_id ]
|
||||
properties {
|
||||
prepare_id {
|
||||
description: "Call ID returned by a call to prepare_download_for_get_all"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,42 @@
|
||||
_description: "Provides a management API for pipelines in the system."
|
||||
_definitions {
|
||||
include "_common.conf"
|
||||
}
|
||||
delete_runs {
|
||||
"2.26": ${_definitions.batch_operation} {
|
||||
description: Delete pipeline runs
|
||||
request {
|
||||
required: [ids, project]
|
||||
properties {
|
||||
ids.description: "IDs of the pipeline runs to delete. Should be the ids of pipeline controller tasks"
|
||||
project {
|
||||
description: "Pipeline project ids. When deleting at least one run should be left"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
properties {
|
||||
succeeded.items.properties.deleted {
|
||||
description: "Indicates whether the task was deleted"
|
||||
type: boolean
|
||||
}
|
||||
succeeded.items.properties.updated_children {
|
||||
description: "Number of child tasks whose parent property was updated"
|
||||
type: integer
|
||||
}
|
||||
succeeded.items.properties.updated_models {
|
||||
description: "Number of models whose task property was updated"
|
||||
type: integer
|
||||
}
|
||||
succeeded.items.properties.deleted_models {
|
||||
description: "Number of deleted output models"
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_pipeline {
|
||||
"2.17" {
|
||||
description: "Start a pipeline"
|
||||
@@ -24,7 +59,7 @@ start_pipeline {
|
||||
type: object
|
||||
properties {
|
||||
name: { type: string }
|
||||
value: { type: [string, null] }
|
||||
value: { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
_description: "Provides support for defining Projects containing Tasks, Models and Dataset Versions."
|
||||
_definitions {
|
||||
include "_common.conf"
|
||||
multi_field_pattern_data {
|
||||
type: object
|
||||
properties {
|
||||
@@ -61,12 +62,26 @@ _definitions {
|
||||
type: string
|
||||
}
|
||||
last_update {
|
||||
description: """Last project update time. Reflects the last time the project metadata was changed or a task in this project has changed status"""
|
||||
description: "Last project update time. Reflects the last time the project metadata was changed or a task in this project has changed status"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
stats_datasets {
|
||||
type: object
|
||||
properties {
|
||||
count {
|
||||
description: Number of datasets
|
||||
type: integer
|
||||
}
|
||||
tags {
|
||||
description: Dataset tags
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats_status_count {
|
||||
type: object
|
||||
properties {
|
||||
@@ -141,6 +156,10 @@ _definitions {
|
||||
description: "Stats for archived tasks"
|
||||
"$ref": "#/definitions/stats_status_count"
|
||||
}
|
||||
datasets {
|
||||
description: "Stats for childrent datasets"
|
||||
"$ref": "#/definitions/stats_datasets"
|
||||
}
|
||||
}
|
||||
}
|
||||
projects_get_all_response_single {
|
||||
@@ -191,11 +210,15 @@ _definitions {
|
||||
type: string
|
||||
}
|
||||
last_update {
|
||||
description: """Last project update time. Reflects the last time the project metadata was changed or a task in this project has changed status"""
|
||||
description: "Last project update time. Reflects the last time the project metadata was changed or a task in this project has changed status"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
// extra properties
|
||||
hidden {
|
||||
description: "Returned if the search_hidden flag was specified in the get_all_ex call and the project is hidden"
|
||||
type: boolean
|
||||
}
|
||||
stats {
|
||||
description: "Additional project stats"
|
||||
"$ref": "#/definitions/stats"
|
||||
@@ -217,6 +240,10 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
own_datasets {
|
||||
description: "The amount of datasets/hyperdatasers under this project (without children projects). Returned if 'check_own_contents' flag is set in the request and children_type is set to 'dataset' or 'hyperdataset'"
|
||||
type: integer
|
||||
}
|
||||
own_tasks {
|
||||
description: "The amount of tasks under this project (without children projects). Returned if 'check_own_contents' flag is set in the request"
|
||||
type: integer
|
||||
@@ -543,7 +570,7 @@ get_all_ex {
|
||||
request {
|
||||
properties {
|
||||
active_users {
|
||||
descritpion: "The list of users that were active in the project. If passes then the resulting projects are filtered to the ones that have tasks created by these users"
|
||||
description: "The list of users that were active in the project. If passes then the resulting projects are filtered to the ones that have tasks created by these users"
|
||||
type: array
|
||||
items: {type: string}
|
||||
}
|
||||
@@ -611,6 +638,38 @@ get_all_ex {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.23": ${get_all_ex."2.20"} {
|
||||
request.properties {
|
||||
allow_public {
|
||||
description: "Allow public projects to be returned in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.24": ${get_all_ex."2.23"} {
|
||||
request.properties.children_type {
|
||||
description: If specified that only the projects under which the entities of this type can be found will be returned
|
||||
type: string
|
||||
enum: [pipeline, report, dataset]
|
||||
}
|
||||
}
|
||||
"2.25": ${get_all_ex."2.24"} {
|
||||
request.properties.children_tags {
|
||||
description: "The list of tag values to filter children by. Takes effect only if children_type is set. Use 'null' value to specify empty tags. Use '__Snot' value to specify that the following value should be excluded"
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
"2.27": ${get_all_ex."2.25"} {
|
||||
request.properties {
|
||||
filters {
|
||||
type: object
|
||||
additionalProperties: ${_definitions.field_filter}
|
||||
}
|
||||
children_tags_filter: ${_definitions.field_filter}
|
||||
}
|
||||
}
|
||||
}
|
||||
update {
|
||||
"2.1" {
|
||||
@@ -759,6 +818,26 @@ validate_delete {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.26": ${validate_delete."2.14"} {
|
||||
response.properties {
|
||||
reports {
|
||||
description: "The total number of reports under the project and all its children"
|
||||
type: integer
|
||||
}
|
||||
non_archived_reports {
|
||||
description: "The total number of non-archived reports under the project and all its children"
|
||||
type: integer
|
||||
}
|
||||
pipelines {
|
||||
description: "The total number of pipelines with active controllers under the project and all its children"
|
||||
type: integer
|
||||
}
|
||||
datasets {
|
||||
description: "The total number of non-empty datasets under the project and all its children"
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete {
|
||||
"2.1" {
|
||||
@@ -819,6 +898,13 @@ delete {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.26": ${delete."2.13"} {
|
||||
request.properties.delete_external_artifacts {
|
||||
description: "If set to 'true' then BE will try to delete the extenal artifacts associated with the project tasks and models from the fileserver (if configured to do so)"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
get_unique_metric_variants {
|
||||
"2.1" {
|
||||
@@ -856,6 +942,13 @@ get_unique_metric_variants {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.25": ${get_unique_metric_variants."2.13"} {
|
||||
request.properties.model_metrics {
|
||||
description: If set to true then bring unique metric and variant names from the project models otherwise from the project tasks
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_hyperparam_values {
|
||||
"2.13" {
|
||||
@@ -903,6 +996,26 @@ get_hyperparam_values {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.26": ${get_hyperparam_values."2.13"} {
|
||||
request.properties {
|
||||
page {
|
||||
description: "Page number"
|
||||
default: 0
|
||||
type: integer
|
||||
}
|
||||
page_size {
|
||||
description: "Page size"
|
||||
default: 500
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.27": ${get_hyperparam_values."2.26"} {
|
||||
request.properties.pattern {
|
||||
type: string
|
||||
description: The search pattern regex
|
||||
}
|
||||
}
|
||||
}
|
||||
get_hyper_parameters {
|
||||
"2.9" {
|
||||
@@ -1000,6 +1113,20 @@ get_model_metadata_values {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.26": ${get_model_metadata_values."2.17"} {
|
||||
request.properties {
|
||||
page {
|
||||
description: "Page number"
|
||||
default: 0
|
||||
type: integer
|
||||
}
|
||||
page_size {
|
||||
description: "Page size"
|
||||
default: 500
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_model_metadata_keys {
|
||||
"2.17" {
|
||||
@@ -1159,13 +1286,15 @@ get_task_parents {
|
||||
}
|
||||
project {
|
||||
type: object
|
||||
id {
|
||||
description: "The ID of the parent task project"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "The name of the parent task project"
|
||||
type: string
|
||||
properties {
|
||||
id {
|
||||
description: "The ID of the parent task project"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "The name of the parent task project"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1185,4 +1314,58 @@ get_task_parents {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.25": ${get_task_parents."2.13"} {
|
||||
request.properties.task_name {
|
||||
description: Task name pattern for the returned parent tasks
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
get_user_names {
|
||||
"2.26" {
|
||||
description: "Get names and ids of the users who created child entitites under the passed projects"
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
projects {
|
||||
description: "The list of projects. If not passed or empty then all the projects are searched"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
include_subprojects {
|
||||
description: "If set to 'true' and the projects field is not empty then the result includes user name from the subprojects children"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
entity {
|
||||
description: The type of the child entity to look for
|
||||
type: string
|
||||
enum: [task, model]
|
||||
default: task
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
users {
|
||||
description: "The list of users sorted by their names"
|
||||
type: array
|
||||
items {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "The ID of the user"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "The name of the user"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,14 @@ get_all_ex {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.27": ${get_all_ex."2.21"} {
|
||||
request.properties {
|
||||
filters {
|
||||
type: object
|
||||
additionalProperties: ${_definitions.field_filter}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all {
|
||||
"2.4" {
|
||||
|
||||
747
apiserver/schema/services/reports.conf
Normal file
747
apiserver/schema/services/reports.conf
Normal file
@@ -0,0 +1,747 @@
|
||||
_description: "Provides a management API for reports in the system."
|
||||
_definitions {
|
||||
include "_tasks_common.conf"
|
||||
include "_events_common.conf"
|
||||
update_response {
|
||||
type: object
|
||||
properties {
|
||||
updated {
|
||||
description: "Number of reports updated (0 or 1)"
|
||||
type: integer
|
||||
enum: [ 0, 1 ]
|
||||
}
|
||||
fields {
|
||||
description: "Updated fields names and values"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
}
|
||||
report_status_enum {
|
||||
type: string
|
||||
enum: [
|
||||
created
|
||||
published
|
||||
]
|
||||
}
|
||||
report {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "Report id"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "Report Name"
|
||||
type: string
|
||||
}
|
||||
user {
|
||||
description: "Associated user id"
|
||||
type: string
|
||||
}
|
||||
company {
|
||||
description: "Company ID"
|
||||
type: string
|
||||
}
|
||||
status {
|
||||
description: ""
|
||||
"$ref": "#/definitions/report_status_enum"
|
||||
}
|
||||
comment {
|
||||
description: "Free text comment"
|
||||
type: string
|
||||
}
|
||||
report {
|
||||
description: "Report template"
|
||||
type: string
|
||||
}
|
||||
report_assets {
|
||||
description: "List of the external report assets"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
created {
|
||||
description: "Report creation time (UTC) "
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
project {
|
||||
description: "Project ID of the project to which this report is assigned"
|
||||
type: string
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags list"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
system_tags {
|
||||
description: "System tags list. This field is reserved for system use, please don't use it."
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
status_changed {
|
||||
description: "Last status change time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
status_message {
|
||||
description: "free text string representing info about the status"
|
||||
type: string
|
||||
}
|
||||
status_reason {
|
||||
description: "Reason for last status change"
|
||||
type: string
|
||||
}
|
||||
published {
|
||||
description: "Report publish time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_update {
|
||||
description: "Last time this report was created, edited, changed"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
create {
|
||||
"2.23" {
|
||||
description: "Create a new report"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
name
|
||||
]
|
||||
properties {
|
||||
name {
|
||||
description: "Report name. Unique within the company."
|
||||
type: string
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags list"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
comment {
|
||||
description: "Free text comment "
|
||||
type: string
|
||||
}
|
||||
report {
|
||||
description: "Report template"
|
||||
type: string
|
||||
}
|
||||
project {
|
||||
description: "Project ID of the project to which this report is assigned Must exist[ab]"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "ID of the report"
|
||||
type: string
|
||||
}
|
||||
project_id {
|
||||
description: "ID of the project that the report belongs to"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.24": ${create."2.23"} {
|
||||
request.properties.report_assets {
|
||||
description: "List of the external report assets"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
update {
|
||||
"2.23" {
|
||||
description: "Create a new report"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
task
|
||||
]
|
||||
properties {
|
||||
task {
|
||||
description: "The ID of the report task to update"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "Report name. Unique within the company."
|
||||
type: string
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags list"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
comment {
|
||||
description: "Free text comment "
|
||||
type: string
|
||||
}
|
||||
report {
|
||||
description: "Report template"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response: ${_definitions.update_response}
|
||||
}
|
||||
"2.24": ${update."2.23"} {
|
||||
request.properties.report_assets {
|
||||
description: "List of the external report assets"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
move {
|
||||
"2.23" {
|
||||
description: "Move reports to a project"
|
||||
request {
|
||||
type: object
|
||||
required: [task]
|
||||
properties {
|
||||
task {
|
||||
description: "ID of the report to move"
|
||||
type: string
|
||||
}
|
||||
project {
|
||||
description: "Target project ID. If not provided, `project_name` must be provided. Use null for the root project"
|
||||
type: string
|
||||
}
|
||||
project_name {
|
||||
description: "Target project name. If provided and a project with this name does not exist, a new project will be created. If not provided, `project` must be provided."
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
project_id: {
|
||||
description: The ID of the target project
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
publish {
|
||||
"2.23" {
|
||||
description: "Publish report"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
task
|
||||
]
|
||||
properties {
|
||||
task {
|
||||
description: "The ID of the report task to publish"
|
||||
type: string
|
||||
}
|
||||
comment {
|
||||
description: "The client message"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response: ${_definitions.update_response}
|
||||
}
|
||||
}
|
||||
archive {
|
||||
"2.23" {
|
||||
description: "Archive report"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
task
|
||||
]
|
||||
properties {
|
||||
task {
|
||||
description: "The ID of the report task to archive"
|
||||
type: string
|
||||
}
|
||||
comment {
|
||||
description: "The client message"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
archived {
|
||||
description: "Number of reports archived (0 or 1)"
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unarchive {
|
||||
"2.23" {
|
||||
description: "Unarchive report"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
task
|
||||
]
|
||||
properties {
|
||||
task {
|
||||
description: "The ID of the report task to unarchive"
|
||||
type: string
|
||||
}
|
||||
comment {
|
||||
description: "The client message"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
unarchived {
|
||||
description: "Number of reports unarchived (0 or 1)"
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//share {
|
||||
// "999.0" {
|
||||
// description: "Share or unshare report"
|
||||
// request {
|
||||
// type: object
|
||||
// required: [
|
||||
// task
|
||||
// ]
|
||||
// properties {
|
||||
// task {
|
||||
// description: "The ID of the report task to share/unshare"
|
||||
// type: string
|
||||
// }
|
||||
// share {
|
||||
// description: "If set to 'true' then the report will be shared. Otherwise unshared."
|
||||
// type: boolean
|
||||
// default: true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// response {
|
||||
// type: object
|
||||
// properties {
|
||||
// changed {
|
||||
// description: "Number of changed reports (0 or 1)"
|
||||
// type: integer
|
||||
// enum: [0, 1]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
delete {
|
||||
"2.23" {
|
||||
description: "Delete report"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
task
|
||||
]
|
||||
properties {
|
||||
task {
|
||||
description: "The ID of the report task to delete"
|
||||
type: string
|
||||
}
|
||||
force {
|
||||
description: "If not set then published or unarchived reports cannot be deleted"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
deleted {
|
||||
description: "Number of deleted reports (0 or 1)"
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_task_data {
|
||||
"2.23" {
|
||||
description: "Get the tasks data according the passed search criteria + requested events"
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "List of IDs to filter by"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
name {
|
||||
description: "Get only tasks whose name matches this pattern (python regular expression syntax)"
|
||||
type: string
|
||||
}
|
||||
user {
|
||||
description: "List of user IDs used to filter results by the task's creating user"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
size {
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: "The number of tasks to retrieve"
|
||||
}
|
||||
order_by {
|
||||
description: "List of field names to order by. When search_text is used, '@text_score' can be used as a field representing the text score of returned documents. Use '-' prefix to specify descending order. Optional, recommended when using page"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
type {
|
||||
description: "List of task types. One or more of: 'import', 'annotation', 'training' or 'testing' (case insensitive)"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
tags {
|
||||
description: "List of task user-defined tags. Use '-' prefix to exclude tags"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
system_tags {
|
||||
description: "List of task system tags. Use '-' prefix to exclude system tags"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
status {
|
||||
description: "List of task status."
|
||||
type: array
|
||||
items { "$ref": "#/definitions/task_status_enum" }
|
||||
}
|
||||
project {
|
||||
description: "List of project IDs"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
only_fields {
|
||||
description: "List of task field names (nesting is supported using '.', e.g. execution.model_labels). If provided, this list defines the query's projection (only these fields will be returned for each result entry)"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
parent {
|
||||
description: "Parent ID"
|
||||
type: string
|
||||
}
|
||||
status_changed {
|
||||
description: "List of status changed constraint strings (utcformat, epoch) with an optional prefix modifier (>, >=, <, <=)"
|
||||
type: array
|
||||
items {
|
||||
type: string
|
||||
pattern: "^(>=|>|<=|<)?.*$"
|
||||
}
|
||||
}
|
||||
search_text {
|
||||
description: "Free text search query"
|
||||
type: string
|
||||
}
|
||||
allow_public {
|
||||
description: "Allow public tasks to be returned in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
_all_ {
|
||||
description: "Multi-field pattern condition (all fields match pattern)"
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
}
|
||||
_any_ {
|
||||
description: "Multi-field pattern condition (any field matches pattern)"
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
}
|
||||
"input.view.entries.dataset" {
|
||||
description: "List of input dataset IDs"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
"input.view.entries.version" {
|
||||
description: "List of input dataset version IDs"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
search_hidden {
|
||||
description: "If set to 'true' then hidden tasks are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
include_subprojects {
|
||||
description: "If set to 'true' and project field is set then tasks from the subprojects are searched too"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
plots {
|
||||
type: object
|
||||
properties {
|
||||
iters {
|
||||
type: integer
|
||||
description: "Max number of latest iterations for which to return plots"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: List of metrics and variants
|
||||
items { "$ref": "#/definitions/metric_variants" }
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_images {
|
||||
type: object
|
||||
properties {
|
||||
iters {
|
||||
type: integer
|
||||
description: "Max number of latest iterations for which to return debug images"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: List of metrics and variants
|
||||
items { "$ref": "#/definitions/metric_variants" }
|
||||
}
|
||||
}
|
||||
}
|
||||
scalar_metrics_iter_histogram {
|
||||
type: object
|
||||
properties {
|
||||
samples {
|
||||
description: "The amount of histogram points to return (0 to return all the points). Optional, the default value is 6000."
|
||||
type: integer
|
||||
}
|
||||
key {
|
||||
description: """
|
||||
Histogram x axis to use:
|
||||
iter - iteration number
|
||||
iso_time - event time as ISO formatted string
|
||||
timestamp - event timestamp as milliseconds since epoch
|
||||
"""
|
||||
"$ref": "#/definitions/scalar_key_enum"
|
||||
}
|
||||
metrics {
|
||||
type: array
|
||||
description: List of metrics and variants
|
||||
items { "$ref": "#/definitions/metric_variants" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
tasks {
|
||||
description: "List of tasks"
|
||||
type: array
|
||||
items { "$ref": "#/definitions/task" }
|
||||
}
|
||||
plots {
|
||||
type: object
|
||||
description: "Plots mapped by metric, variant, task and iteration"
|
||||
additionalProperties: true
|
||||
}
|
||||
debug_images {
|
||||
type: array
|
||||
description: "Debug image events grouped by tasks and iterations"
|
||||
items {"$ref": "#/definitions/debug_images_response_task_metrics"}
|
||||
}
|
||||
scalar_metrics_iter_histogram {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.25": ${get_task_data."2.23"} {
|
||||
request.properties {
|
||||
model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
single_value_metrics {
|
||||
type: object
|
||||
description: If passed then task single value metrics are returned
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
response.properties.single_value_metrics {
|
||||
type: array
|
||||
description: Single value metrics grouped by task
|
||||
items {"$ref": "#/definitions/single_value_task_metrics"}
|
||||
}
|
||||
}
|
||||
"2.26": ${get_task_data."2.25"} {
|
||||
request.properties.plots.properties.last_iters_per_task_metric {
|
||||
type: boolean
|
||||
description: If set to 'true' and iters passed then last iterations for each task metrics are retrieved. Otherwise last iterations for the whole task are retrieved
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all_ex {
|
||||
"2.23" {
|
||||
description: "Get all the company's and public report tasks"
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "List of IDs to filter by"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
name {
|
||||
description: "Get only reports whose name matches this pattern (python regular expression syntax)"
|
||||
type: string
|
||||
}
|
||||
user {
|
||||
description: "List of user IDs used to filter results by the reports's creating user"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
page {
|
||||
description: "Page number, returns a specific page out of the resulting list of reports"
|
||||
type: integer
|
||||
minimum: 0
|
||||
}
|
||||
page_size {
|
||||
description: "Page size, specifies the number of results returned in each page (last page may contain fewer results)"
|
||||
type: integer
|
||||
minimum: 1
|
||||
}
|
||||
order_by {
|
||||
description: "List of field names to order by. When search_text is used, '@text_score' can be used as a field representing the text score of returned documents. Use '-' prefix to specify descending order. Optional, recommended when using page"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
tags {
|
||||
description: "List of report user-defined tags. Use '-' prefix to exclude tags"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
system_tags {
|
||||
description: "List of report system tags. Use '-' prefix to exclude system tags"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
status {
|
||||
description: "List of report status."
|
||||
type: array
|
||||
items { "$ref": "#/definitions/report_status_enum" }
|
||||
}
|
||||
project {
|
||||
description: "List of project IDs"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
only_fields {
|
||||
description: "List of report field names (nesting is supported using '.'). If provided, this list defines the query's projection (only these fields will be returned for each result entry)"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
status_changed {
|
||||
description: "List of status changed constraint strings (utcformat, epoch) with an optional prefix modifier (>, >=, <, <=)"
|
||||
type: array
|
||||
items {
|
||||
type: string
|
||||
pattern: "^(>=|>|<=|<)?.*$"
|
||||
}
|
||||
}
|
||||
search_text {
|
||||
description: "Free text search query"
|
||||
type: string
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID returned from the previos calls to get_all"
|
||||
}
|
||||
refresh_scroll {
|
||||
type: boolean
|
||||
description: "If set then all the data received with this scroll will be requeried"
|
||||
}
|
||||
size {
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: "The number of tasks to retrieve"
|
||||
}
|
||||
allow_public {
|
||||
description: "Allow public reports to be returned in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
_all_ {
|
||||
description: "Multi-field pattern condition (all fields match pattern)"
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
}
|
||||
_any_ {
|
||||
description: "Multi-field pattern condition (any field matches pattern)"
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
tasks {
|
||||
description: "List of report tasks"
|
||||
type: array
|
||||
items { "$ref": "#/definitions/report" }
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID that can be used with the next calls to get_all to retrieve more data"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.26": ${get_all_ex."2.23"} {
|
||||
request.properties.include_subprojects {
|
||||
description: "If set to 'true' and project field is set then reports from the subprojects are searched too"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.27": ${get_all_ex."2.26"} {
|
||||
request.properties {
|
||||
filters {
|
||||
type: object
|
||||
additionalProperties: ${_definitions.field_filter}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_tags {
|
||||
"2.23" {
|
||||
description: "Get all the user tags used for the company reports"
|
||||
request {
|
||||
type: object
|
||||
additionalProperties: false
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
tags {
|
||||
description: "The list of unique tag values"
|
||||
type: array
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ _references {
|
||||
}
|
||||
}
|
||||
_definitions {
|
||||
include "_common.conf"
|
||||
include "_tasks_common.conf"
|
||||
change_many_request: ${_definitions.batch_operation} {
|
||||
request {
|
||||
properties {
|
||||
@@ -69,374 +69,6 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
multi_field_pattern_data {
|
||||
type: object
|
||||
properties {
|
||||
pattern {
|
||||
description: "Pattern string (regex)"
|
||||
type: string
|
||||
}
|
||||
fields {
|
||||
description: "List of field names"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
model_type_enum {
|
||||
type: string
|
||||
enum: ["input", "output"]
|
||||
}
|
||||
task_model_item {
|
||||
type: object
|
||||
required: [ name, model]
|
||||
properties {
|
||||
name {
|
||||
description: "The task model name"
|
||||
type: string
|
||||
}
|
||||
model {
|
||||
description: "The model ID"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
script {
|
||||
type: object
|
||||
properties {
|
||||
binary {
|
||||
description: "Binary to use when running the script"
|
||||
type: string
|
||||
default: python
|
||||
}
|
||||
repository {
|
||||
description: "Name of the repository where the script is located"
|
||||
type: string
|
||||
}
|
||||
tag {
|
||||
description: "Repository tag"
|
||||
type: string
|
||||
}
|
||||
branch {
|
||||
description: "Repository branch id If not provided and tag not provided, default repository branch is used."
|
||||
type: string
|
||||
}
|
||||
version_num {
|
||||
description: "Version (changeset) number. Optional (default is head version) Unused if tag is provided."
|
||||
type: string
|
||||
}
|
||||
entry_point {
|
||||
description: "Path to execute within the repository"
|
||||
type: string
|
||||
}
|
||||
working_dir {
|
||||
description: "Path to the folder from which to run the script Default - root folder of repository"
|
||||
type: string
|
||||
}
|
||||
requirements {
|
||||
description: "A JSON object containing requirements strings by key"
|
||||
type: object
|
||||
}
|
||||
diff {
|
||||
description: "Uncommitted changes found in the repository when task was run"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
output {
|
||||
type: object
|
||||
properties {
|
||||
destination {
|
||||
description: "Storage id. This is where output files will be stored."
|
||||
type: string
|
||||
}
|
||||
model {
|
||||
description: "Model id."
|
||||
type: string
|
||||
}
|
||||
result {
|
||||
description: "Task result. Values: 'success', 'failure'"
|
||||
type: string
|
||||
}
|
||||
error {
|
||||
description: "Last error text"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
task_execution_progress_enum {
|
||||
type: string
|
||||
enum: [
|
||||
unknown
|
||||
running
|
||||
stopping
|
||||
stopped
|
||||
]
|
||||
}
|
||||
output_rois_enum {
|
||||
type: string
|
||||
enum: [
|
||||
all_in_frame
|
||||
only_filtered
|
||||
frame_per_roi
|
||||
]
|
||||
}
|
||||
artifact_type_data {
|
||||
type: object
|
||||
properties {
|
||||
preview {
|
||||
description: "Description or textual data"
|
||||
type: string
|
||||
}
|
||||
content_type {
|
||||
description: "System defined raw data content type"
|
||||
type: string
|
||||
}
|
||||
data_hash {
|
||||
description: "Hash of raw data, without any headers or descriptive parts"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
artifact_mode_enum {
|
||||
type: string
|
||||
enum: [
|
||||
input
|
||||
output
|
||||
]
|
||||
default: output
|
||||
}
|
||||
artifact {
|
||||
type: object
|
||||
required: [key, type]
|
||||
properties {
|
||||
key {
|
||||
description: "Entry key"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "System defined type"
|
||||
type: string
|
||||
}
|
||||
mode {
|
||||
description: "System defined input/output indication"
|
||||
"$ref": "#/definitions/artifact_mode_enum"
|
||||
}
|
||||
uri {
|
||||
description: "Raw data location"
|
||||
type: string
|
||||
}
|
||||
content_size {
|
||||
description: "Raw data length in bytes"
|
||||
type: integer
|
||||
}
|
||||
hash {
|
||||
description: "Hash of entire raw data"
|
||||
type: string
|
||||
}
|
||||
timestamp {
|
||||
description: "Epoch time when artifact was created"
|
||||
type: integer
|
||||
}
|
||||
type_data {
|
||||
description: "Additional fields defined by the system"
|
||||
"$ref": "#/definitions/artifact_type_data"
|
||||
}
|
||||
display_data {
|
||||
description: "User-defined list of key/value pairs, sorted"
|
||||
type: array
|
||||
items {
|
||||
type: array
|
||||
items {
|
||||
type: string # can also be a number... TODO: upgrade the generator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
artifact_id {
|
||||
type: object
|
||||
required: [key]
|
||||
properties {
|
||||
key {
|
||||
description: "Entry key"
|
||||
type: string
|
||||
}
|
||||
mode {
|
||||
description: "System defined input/output indication"
|
||||
"$ref": "#/definitions/artifact_mode_enum"
|
||||
}
|
||||
}
|
||||
}
|
||||
task_models {
|
||||
type: object
|
||||
properties {
|
||||
input {
|
||||
description: "The list of task input models"
|
||||
type: array
|
||||
items {"$ref": "#/definitions/task_model_item"}
|
||||
|
||||
}
|
||||
output {
|
||||
description: "The list of task output models"
|
||||
type: array
|
||||
items {"$ref": "#/definitions/task_model_item"}
|
||||
}
|
||||
}
|
||||
}
|
||||
execution {
|
||||
type: object
|
||||
properties {
|
||||
queue {
|
||||
description: "Queue ID where task was queued."
|
||||
type: string
|
||||
}
|
||||
parameters {
|
||||
description: "Json object containing the Task parameters"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
model {
|
||||
description: "Execution input model ID Not applicable for Register (Import) tasks"
|
||||
type: string
|
||||
}
|
||||
model_desc {
|
||||
description: "Json object representing the Model descriptors"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
model_labels {
|
||||
description: """Json object representing the ids of the labels in the model.
|
||||
The keys are the layers' names and the values are the IDs.
|
||||
Not applicable for Register (Import) tasks.
|
||||
Mandatory for Training tasks"""
|
||||
type: object
|
||||
additionalProperties: { type: integer }
|
||||
}
|
||||
framework {
|
||||
description: """Framework related to the task. Case insensitive. Mandatory for Training tasks. """
|
||||
type: string
|
||||
}
|
||||
docker_cmd {
|
||||
description: "Command for running docker script for the execution of the task"
|
||||
type: string
|
||||
}
|
||||
artifacts {
|
||||
description: "Task artifacts"
|
||||
type: array
|
||||
items { "$ref": "#/definitions/artifact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
task_status_enum {
|
||||
type: string
|
||||
enum: [
|
||||
created
|
||||
queued
|
||||
in_progress
|
||||
stopped
|
||||
published
|
||||
publishing
|
||||
closed
|
||||
failed
|
||||
completed
|
||||
unknown
|
||||
]
|
||||
}
|
||||
task_type_enum {
|
||||
type: string
|
||||
enum: [
|
||||
training
|
||||
testing
|
||||
inference
|
||||
data_processing
|
||||
application
|
||||
monitor
|
||||
controller
|
||||
optimizer
|
||||
service
|
||||
qc
|
||||
custom
|
||||
]
|
||||
}
|
||||
last_metrics_event {
|
||||
type: object
|
||||
properties {
|
||||
metric {
|
||||
description: "Metric name"
|
||||
type: string
|
||||
}
|
||||
variant {
|
||||
description: "Variant name"
|
||||
type: string
|
||||
}
|
||||
value {
|
||||
description: "Last value reported"
|
||||
type: number
|
||||
}
|
||||
min_value {
|
||||
description: "Minimum value reported"
|
||||
type: number
|
||||
}
|
||||
max_value {
|
||||
description: "Maximum value reported"
|
||||
type: number
|
||||
}
|
||||
}
|
||||
}
|
||||
last_metrics_variants {
|
||||
type: object
|
||||
description: "Last metric events, one for each variant hash"
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/last_metrics_event"
|
||||
}
|
||||
}
|
||||
params_item {
|
||||
type: object
|
||||
properties {
|
||||
section {
|
||||
description: "Section that the parameter belongs to"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "Name of the parameter. The combination of section and name should be unique"
|
||||
type: string
|
||||
}
|
||||
value {
|
||||
description: "Value of the parameter"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "Type of the parameter. Optional"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "The parameter description. Optional"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
configuration_item {
|
||||
type: object
|
||||
properties {
|
||||
name {
|
||||
description: "Name of the parameter. Should be unique"
|
||||
type: string
|
||||
}
|
||||
value {
|
||||
description: "Value of the parameter"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "Type of the parameter. Optional"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "The parameter description. Optional"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
param_key {
|
||||
type: object
|
||||
properties {
|
||||
@@ -450,13 +82,6 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
section_params {
|
||||
description: "Task section params"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/params_item"
|
||||
}
|
||||
}
|
||||
replace_hyperparams_enum {
|
||||
type: string
|
||||
enum: [
|
||||
@@ -465,165 +90,6 @@ _definitions {
|
||||
all
|
||||
]
|
||||
}
|
||||
task {
|
||||
type: object
|
||||
properties {
|
||||
id {
|
||||
description: "Task id"
|
||||
type: string
|
||||
}
|
||||
name {
|
||||
description: "Task Name"
|
||||
type: string
|
||||
}
|
||||
user {
|
||||
description: "Associated user id"
|
||||
type: string
|
||||
}
|
||||
company {
|
||||
description: "Company ID"
|
||||
type: string
|
||||
}
|
||||
type {
|
||||
description: "Type of task. Values: 'training', 'testing'"
|
||||
"$ref": "#/definitions/task_type_enum"
|
||||
}
|
||||
status {
|
||||
description: ""
|
||||
"$ref": "#/definitions/task_status_enum"
|
||||
}
|
||||
comment {
|
||||
description: "Free text comment"
|
||||
type: string
|
||||
}
|
||||
created {
|
||||
description: "Task creation time (UTC) "
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
started {
|
||||
description: "Task start time (UTC)"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
completed {
|
||||
description: "Task end time (UTC)"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
active_duration {
|
||||
description: "Task duration time (seconds)"
|
||||
type: integer
|
||||
}
|
||||
parent {
|
||||
description: "Parent task id"
|
||||
type: string
|
||||
}
|
||||
project {
|
||||
description: "Project ID of the project to which this task is assigned"
|
||||
type: string
|
||||
}
|
||||
output {
|
||||
description: "Task output params"
|
||||
"$ref": "#/definitions/output"
|
||||
}
|
||||
execution {
|
||||
description: "Task execution params"
|
||||
"$ref": "#/definitions/execution"
|
||||
}
|
||||
container {
|
||||
description: "Docker container parameters"
|
||||
type: object
|
||||
additionalProperties { type: [string, null] }
|
||||
}
|
||||
models {
|
||||
description: "Task models"
|
||||
"$ref": "#/definitions/task_models"
|
||||
}
|
||||
// TODO: will be removed
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags list"
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
system_tags {
|
||||
description: "System tags list. This field is reserved for system use, please don't use it."
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
status_changed {
|
||||
description: "Last status change time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
status_message {
|
||||
description: "free text string representing info about the status"
|
||||
type: string
|
||||
}
|
||||
status_reason {
|
||||
description: "Reason for last status change"
|
||||
type: string
|
||||
}
|
||||
published {
|
||||
description: "Last status change time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_worker {
|
||||
description: "ID of last worker that handled the task"
|
||||
type: string
|
||||
}
|
||||
last_worker_report {
|
||||
description: "Last time a worker reported while working on this task"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_update {
|
||||
description: "Last time this task was created, updated, changed or events for this task were reported"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_change {
|
||||
description: "Last time any update was done to the task"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_iteration {
|
||||
description: "Last iteration reported for this task"
|
||||
type: integer
|
||||
}
|
||||
last_metrics {
|
||||
description: "Last metric variants (hash to events), one for each metric hash"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/last_metrics_variants"
|
||||
}
|
||||
}
|
||||
hyperparams {
|
||||
description: "Task hyper params per section"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/section_params"
|
||||
}
|
||||
}
|
||||
configuration {
|
||||
description: "Task configuration params"
|
||||
type: object
|
||||
additionalProperties {
|
||||
"$ref": "#/definitions/configuration_item"
|
||||
}
|
||||
}
|
||||
runtime {
|
||||
description: "Task runtime mapping"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
}
|
||||
task_urls {
|
||||
type: object
|
||||
properties {
|
||||
@@ -715,6 +181,23 @@ get_all_ex {
|
||||
description: "Scroll ID that can be used with the next calls to get_all_ex to retrieve more data"
|
||||
}
|
||||
}
|
||||
"2.23": ${get_all_ex."2.15"} {
|
||||
request.properties {
|
||||
allow_public {
|
||||
description: "Allow public tasks to be returned in the results"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.27": ${get_all_ex."2.23"} {
|
||||
request.properties {
|
||||
filters {
|
||||
type: object
|
||||
additionalProperties: ${_definitions.field_filter}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all {
|
||||
"2.1" {
|
||||
@@ -814,9 +297,6 @@ get_all {
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
page: [ page_size ]
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
@@ -859,6 +339,17 @@ get_all {
|
||||
description: "Scroll ID that can be used with the next calls to get_all to retrieve more data"
|
||||
}
|
||||
}
|
||||
"2.26": ${get_all."2.15"} {
|
||||
request {
|
||||
properties {
|
||||
include_subprojects {
|
||||
description: "If set to 'true' and project field is set then tasks from the subprojects are searched too"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_types {
|
||||
"2.8" {
|
||||
@@ -995,7 +486,7 @@ clone {
|
||||
new_task_container {
|
||||
description: "The docker container properties for the new task. If not provided then taken from the original task"
|
||||
type: object
|
||||
additionalProperties { type: [string, null] }
|
||||
additionalProperties { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1173,7 +664,7 @@ create {
|
||||
container {
|
||||
description: "Docker container parameters"
|
||||
type: object
|
||||
additionalProperties { type: [string, null] }
|
||||
additionalProperties { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1227,6 +718,10 @@ validate {
|
||||
description: "Task execution params"
|
||||
"$ref": "#/definitions/execution"
|
||||
}
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
hyperparams {
|
||||
description: "Task hyper params per section"
|
||||
type: object
|
||||
@@ -1241,10 +736,6 @@ validate {
|
||||
"$ref": "#/definitions/configuration_item"
|
||||
}
|
||||
}
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
@@ -1262,7 +753,7 @@ validate {
|
||||
container {
|
||||
description: "Docker container parameters"
|
||||
type: object
|
||||
additionalProperties { type: [string, null] }
|
||||
additionalProperties { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1392,6 +883,10 @@ edit {
|
||||
description: "Task execution params"
|
||||
"$ref": "#/definitions/execution"
|
||||
}
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
hyperparams {
|
||||
description: "Task hyper params per section"
|
||||
type: object
|
||||
@@ -1406,10 +901,6 @@ edit {
|
||||
"$ref": "#/definitions/configuration_item"
|
||||
}
|
||||
}
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
}
|
||||
}
|
||||
response: ${_definitions.update_response}
|
||||
@@ -1424,7 +915,7 @@ edit {
|
||||
container {
|
||||
description: "Docker container parameters"
|
||||
type: object
|
||||
additionalProperties { type: [string, null] }
|
||||
additionalProperties { type: string }
|
||||
}
|
||||
runtime {
|
||||
description: "Task runtime mapping"
|
||||
@@ -1478,6 +969,10 @@ reset {
|
||||
description: "If set to 'true' then return the urls of the files that were uploaded by this task. Default value is 'false'"
|
||||
type: boolean
|
||||
}
|
||||
delete_output_models {
|
||||
description: "If set to 'true' then delete output models of this task that are not referenced by other tasks. Default value is 'true'"
|
||||
type: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
@@ -1665,6 +1160,10 @@ delete {
|
||||
description: "If set to 'true' then return the urls of the files that were uploaded by this task. Default value is 'false'"
|
||||
type: boolean
|
||||
}
|
||||
delete_output_models {
|
||||
description: "If set to 'true' then delete output models of this task that are not referenced by other tasks. Default value is 'true'"
|
||||
type: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
@@ -1736,12 +1235,12 @@ archive_many {
|
||||
type: string
|
||||
}
|
||||
}
|
||||
response {
|
||||
properties {
|
||||
succeeded.items.properties.archived {
|
||||
description: "Indicates whether the task was archived"
|
||||
type: boolean
|
||||
}
|
||||
}
|
||||
response {
|
||||
properties {
|
||||
succeeded.items.properties.archived {
|
||||
description: "Indicates whether the task was archived"
|
||||
type: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2024,6 +1523,19 @@ dequeue {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.25": ${dequeue."1.5"} {
|
||||
request.properties.remove_from_all_queues {
|
||||
type: boolean
|
||||
description: If set to 'true' then the task is searched and removed from all the queues. Otherwise only from the queue stored in the task execution parameters
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.26": ${dequeue."2.25"} {
|
||||
request.properties.new_status {
|
||||
type: string
|
||||
description: The new status to assign to the task after the dequeue instead of the default one
|
||||
}
|
||||
}
|
||||
}
|
||||
dequeue_many {
|
||||
"2.13": ${_definitions.change_many_request} {
|
||||
@@ -2042,6 +1554,19 @@ dequeue_many {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.25": ${dequeue_many."2.13"} {
|
||||
request.properties.remove_from_all_queues {
|
||||
type: boolean
|
||||
description: If set to 'true' then the tasks are searched and removed from all the queues. Otherwise only from the queue stored in the task execution parameters
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.26": ${dequeue_many."2.25"} {
|
||||
request.properties.new_status {
|
||||
type: string
|
||||
description: The new status to assign to the task after the dequeue instead of the default one
|
||||
}
|
||||
}
|
||||
}
|
||||
set_requirements {
|
||||
"2.1" {
|
||||
@@ -2515,7 +2040,7 @@ move {
|
||||
items { type: string }
|
||||
}
|
||||
project {
|
||||
description: "Target project ID. If not provided, `project_name` must be provided."
|
||||
description: "Target project ID. If not provided, `project_name` must be provided. Use null for the root project"
|
||||
type: string
|
||||
}
|
||||
project_name {
|
||||
@@ -2530,3 +2055,37 @@ move {
|
||||
}
|
||||
}
|
||||
}
|
||||
update_tags {
|
||||
"2.27" {
|
||||
description: Add or remove tags from multiple tasks
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
ids {
|
||||
type: array
|
||||
description: IDs of the tasks to update
|
||||
items {type: string}
|
||||
}
|
||||
add_tags {
|
||||
type: array
|
||||
description: User tags to add
|
||||
items {type: string}
|
||||
}
|
||||
remove_tags {
|
||||
type: array
|
||||
description: User tags to remove
|
||||
items {type: string}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
updated {
|
||||
type: integer
|
||||
description: The number of updated tasks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,17 @@ get_current_user {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.26": ${get_current_user."2.20"} {
|
||||
response.properties.settings {
|
||||
type: object
|
||||
properties {
|
||||
max_download_items {
|
||||
type: string
|
||||
description: The maximum items downloaded for this user in csv file downloads
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_all_ex {
|
||||
|
||||
@@ -311,6 +311,41 @@ get_all {
|
||||
}
|
||||
}
|
||||
}
|
||||
get_count {
|
||||
"2.26": {
|
||||
description: "Returns the number of registered workers."
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
last_seen {
|
||||
description: """Filter out workers not active for more than last_seen seconds.
|
||||
A value or 0 or 'none' will disable the filter."""
|
||||
type: integer
|
||||
default: 0
|
||||
}
|
||||
tags {
|
||||
description: The list of allowed worker tags. Prepend tag value with '-' in order to exclude
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
system_tags {
|
||||
description: The list of allowed worker system tags. Prepend tag value with '-' in order to exclude
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
count {
|
||||
description: Workers count
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
register {
|
||||
"2.4" {
|
||||
description: "Register a worker in the system. Called by the Worker Daemon."
|
||||
|
||||
@@ -46,7 +46,6 @@ class AppSequence:
|
||||
self._attach_request_handlers(request_handlers)
|
||||
|
||||
def _attach_request_handlers(self, request_handlers: RequestHandlers):
|
||||
self.app.before_first_request(request_handlers.before_app_first_request)
|
||||
self.app.before_request(request_handlers.before_request)
|
||||
self.app.after_request(request_handlers.after_request)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
from functools import partial
|
||||
|
||||
from flask import request, Response, redirect
|
||||
@@ -20,9 +22,6 @@ class RequestHandlers:
|
||||
_request_strip_prefix = config.get("apiserver.request.strip_prefix", None)
|
||||
_server_header = config.get("apiserver.response.headers.server", "clearml")
|
||||
|
||||
def before_app_first_request(self):
|
||||
pass
|
||||
|
||||
def before_request(self):
|
||||
if request.method == "OPTIONS":
|
||||
return "", 200
|
||||
@@ -44,9 +43,17 @@ class RequestHandlers:
|
||||
else:
|
||||
headers = None
|
||||
if call.result.filename:
|
||||
headers = {
|
||||
"Content-Disposition": f"attachment; filename={call.result.filename}"
|
||||
}
|
||||
try:
|
||||
call.result.filename.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
simple = unicodedata.normalize("NFKD", call.result.filename)
|
||||
simple = simple.encode("ascii", "ignore").decode("ascii")
|
||||
# safe = RFC 5987 attr-char
|
||||
quoted = urllib.parse.quote(call.result.filename, safe="")
|
||||
filenames = f"filename={simple}; filename*=UTF-8''{quoted}"
|
||||
else:
|
||||
filenames = f"filename={call.result.filename}"
|
||||
headers = {"Content-Disposition": "attachment; " + filenames}
|
||||
|
||||
response = Response(
|
||||
content,
|
||||
|
||||
@@ -655,7 +655,11 @@ class APICall(DataContainer):
|
||||
}
|
||||
if self.content_type.lower() == JSON_CONTENT_TYPE:
|
||||
try:
|
||||
func = json.dumps if self._json_flags.pop("ensure_ascii", True) else json.dumps_notascii
|
||||
func = (
|
||||
json.dumps
|
||||
if self._json_flags.pop("ensure_ascii", True)
|
||||
else json.dumps_notascii
|
||||
)
|
||||
res = func(res, **(self._json_flags or {}))
|
||||
except Exception as ex:
|
||||
# JSON serialization may fail, probably problem with data or error_data so pop it and try again
|
||||
@@ -685,8 +689,12 @@ class APICall(DataContainer):
|
||||
cookies=self._result.cookies,
|
||||
)
|
||||
|
||||
def get_redacted_headers(self):
|
||||
headers = self.headers.copy()
|
||||
def get_redacted_headers(self, fields=None):
|
||||
headers = (
|
||||
{k: v for k, v in self._headers.items() if k in fields}
|
||||
if fields
|
||||
else self.headers
|
||||
)
|
||||
if not self.requires_authorization or self.auth:
|
||||
# We won't log the authorization header if call shouldn't be authorized, or if it was successfully
|
||||
# authorized. This means we'll only log authorization header for calls that failed to authorize (hopefully
|
||||
|
||||
@@ -30,24 +30,35 @@ def get_auth_func(auth_type):
|
||||
raise errors.unauthorized.BadAuthType()
|
||||
|
||||
|
||||
def authorize_token(jwt_token, *_, **__):
|
||||
def authorize_token(jwt_token, service, action, call):
|
||||
"""Validate token against service/endpoint and requests data (dicts).
|
||||
Returns a parsed token object (auth payload)
|
||||
"""
|
||||
call_info = {"ip": call.real_ip}
|
||||
|
||||
def log_error(msg):
|
||||
info = ", ".join(f"{k}={v}" for k, v in call_info.items())
|
||||
log.error(f"{msg} Call info: {info}")
|
||||
|
||||
try:
|
||||
return Token.from_encoded_token(jwt_token)
|
||||
|
||||
except jwt.exceptions.InvalidKeyError as ex:
|
||||
log_error("Failed parsing token.")
|
||||
raise errors.unauthorized.InvalidToken(
|
||||
"jwt invalid key error", reason=ex.args[0]
|
||||
)
|
||||
except jwt.InvalidTokenError as ex:
|
||||
log_error("Failed parsing token.")
|
||||
raise errors.unauthorized.InvalidToken("invalid jwt token", reason=ex.args[0])
|
||||
except ValueError as ex:
|
||||
log.exception("Failed while processing token: %s" % ex.args[0])
|
||||
log_error(f"Failed while processing token: {str(ex.args[0])}.")
|
||||
raise errors.unauthorized.InvalidToken(
|
||||
"failed processing token", reason=ex.args[0]
|
||||
)
|
||||
except Exception:
|
||||
log_error("Failed processing token.")
|
||||
raise
|
||||
|
||||
|
||||
def authorize_credentials(auth_data, service, action, call):
|
||||
|
||||
@@ -90,7 +90,7 @@ class Token(Payload):
|
||||
return token
|
||||
except Exception as e:
|
||||
raise errors.unauthorized.InvalidToken(
|
||||
"failed parsing token, %s" % e.args[0]
|
||||
"failed parsing token", reason=e.args[0]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -39,7 +39,7 @@ class ServiceRepo(object):
|
||||
"""If the check is set, parsing will fail for endpoint request with the version that is grater than the current
|
||||
maximum """
|
||||
|
||||
_max_version = PartialVersion("2.22")
|
||||
_max_version = PartialVersion("2.27")
|
||||
""" Maximum version number (the highest min_version value across all endpoints) """
|
||||
|
||||
_endpoint_exp = (
|
||||
|
||||
@@ -17,7 +17,7 @@ log = config.logger(__file__)
|
||||
def validate_data(call: APICall, endpoint: Endpoint):
|
||||
""" Perform all required call/endpoint validation, update call result appropriately """
|
||||
try:
|
||||
# todo: remove vaildate_required_fields once all endpoints have json schema
|
||||
# todo: remove validate_required_fields once all endpoints have json schema
|
||||
validate_required_fields(endpoint, call)
|
||||
|
||||
# set models. models will be validated automatically
|
||||
@@ -50,10 +50,17 @@ def validate_role(endpoint, call):
|
||||
pass
|
||||
|
||||
|
||||
def validate_auth(endpoint, call):
|
||||
""" Validate authorization for this endpoint and call.
|
||||
If authentication has occurred, the call is updated with the authentication results.
|
||||
def validate_auth(endpoint: Endpoint, call: "APICall"):
|
||||
"""
|
||||
Validate authorization for this endpoint and call.
|
||||
If authentication has occurred, the call is updated with the authentication results.
|
||||
For the endpoints with authorize==False the validation is not performed to improve performance
|
||||
For the endpoints with authorize==True the validation should pass otherwise exception will be thrown
|
||||
For the endpoints with authorize==None the validation will be tried, but it does not have to succeed
|
||||
"""
|
||||
if endpoint.authorize is not None and not endpoint.authorize:
|
||||
return
|
||||
|
||||
if not call.authorization:
|
||||
# No auth data. Invalid if we need to authorize and valid otherwise
|
||||
if endpoint.authorize:
|
||||
@@ -63,10 +70,9 @@ def validate_auth(endpoint, call):
|
||||
# prepare arguments for validation
|
||||
service, _, action = endpoint.name.partition(".")
|
||||
|
||||
# If we have auth data, we'll try to validate anyway (just so we'll have auth-based permissions whenever possible,
|
||||
# even if endpoint did not require authorization)
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
auth = call.authorization or ""
|
||||
auth = call.authorization
|
||||
auth_type, _, auth_data = auth.partition(" ")
|
||||
authorize_func = get_auth_func(auth_type)
|
||||
call.auth = authorize_func(auth_data, service, action, call)
|
||||
@@ -78,7 +84,7 @@ def validate_auth(endpoint, call):
|
||||
|
||||
def validate_impersonation(endpoint, call):
|
||||
""" Validate impersonation headers and set impersonated identity and authorization data accordingly.
|
||||
:returns True if impersonating, False otherwise
|
||||
:return: True if impersonating, False otherwise
|
||||
"""
|
||||
try:
|
||||
act_as = call.act_as
|
||||
|
||||
@@ -3,4 +3,7 @@ from apiserver.service_repo import APICall, endpoint
|
||||
|
||||
@endpoint("debug.ping")
|
||||
def ping(call: APICall, _, __):
|
||||
call.result.data = {"msg": "ClearML server"}
|
||||
res = {"msg": "ClearML server"}
|
||||
if call.data:
|
||||
res.update(call.data)
|
||||
call.result.data = res
|
||||
|
||||
@@ -2,7 +2,7 @@ import itertools
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from typing import Sequence, Optional, Union, Tuple
|
||||
from typing import Sequence, Optional, Union, Tuple, Mapping
|
||||
|
||||
import attr
|
||||
import jsonmodels.fields
|
||||
@@ -27,10 +27,13 @@ from apiserver.apimodels.events import (
|
||||
ClearScrollRequest,
|
||||
ClearTaskLogRequest,
|
||||
SingleValueMetricsRequest,
|
||||
GetVariantSampleRequest, GetMetricSamplesRequest,
|
||||
GetVariantSampleRequest,
|
||||
GetMetricSamplesRequest,
|
||||
TaskMetric,
|
||||
MultiTaskPlotsRequest,
|
||||
)
|
||||
from apiserver.bll.event import EventBLL
|
||||
from apiserver.bll.event.event_common import EventType, MetricVariants
|
||||
from apiserver.bll.event.event_common import EventType, MetricVariants, TaskCompanies
|
||||
from apiserver.bll.event.events_iterator import Scroll
|
||||
from apiserver.bll.event.scalar_key import ScalarKeyEnum, ScalarKey
|
||||
from apiserver.bll.model import ModelBLL
|
||||
@@ -68,9 +71,11 @@ def _assert_task_or_model_exists(
|
||||
@endpoint("events.add")
|
||||
def add(call: APICall, company_id, _):
|
||||
data = call.data.copy()
|
||||
allow_locked = data.pop("allow_locked", False)
|
||||
added, err_count, err_info = event_bll.add_events(
|
||||
company_id, [data], call.worker, allow_locked=allow_locked
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
events=[data],
|
||||
worker=call.worker,
|
||||
)
|
||||
call.result.data = dict(added=added, errors=err_count, errors_info=err_info)
|
||||
|
||||
@@ -82,10 +87,10 @@ def add_batch(call: APICall, company_id, _):
|
||||
raise errors.bad_request.BatchContainsNoItems()
|
||||
|
||||
added, err_count, err_info = event_bll.add_events(
|
||||
company_id,
|
||||
events,
|
||||
call.worker,
|
||||
allow_locked=events[0].get("allow_locked", False),
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
events=events,
|
||||
worker=call.worker,
|
||||
)
|
||||
call.result.data = dict(added=added, errors=err_count, errors_info=err_info)
|
||||
|
||||
@@ -255,7 +260,9 @@ def get_vector_metrics_and_variants(call, company_id, _):
|
||||
task_id = call.data["task"]
|
||||
model_events = call.data["model_events"]
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=model_events,
|
||||
company_id,
|
||||
task_id,
|
||||
model_events=model_events,
|
||||
)[0]
|
||||
call.result.data = dict(
|
||||
metrics=event_bll.get_metrics_and_variants(
|
||||
@@ -269,7 +276,9 @@ def get_scalar_metrics_and_variants(call, company_id, _):
|
||||
task_id = call.data["task"]
|
||||
model_events = call.data["model_events"]
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=model_events,
|
||||
company_id,
|
||||
task_id,
|
||||
model_events=model_events,
|
||||
)[0]
|
||||
call.result.data = dict(
|
||||
metrics=event_bll.get_metrics_and_variants(
|
||||
@@ -287,7 +296,9 @@ def vector_metrics_iter_histogram(call, company_id, _):
|
||||
task_id = call.data["task"]
|
||||
model_events = call.data["model_events"]
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=model_events,
|
||||
company_id,
|
||||
task_id,
|
||||
model_events=model_events,
|
||||
)[0]
|
||||
metric = call.data["metric"]
|
||||
variant = call.data["variant"]
|
||||
@@ -320,7 +331,9 @@ def make_response(
|
||||
def get_task_events(_, company_id, request: TaskEventsRequest):
|
||||
task_id = request.task
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=request.model_events,
|
||||
company_id,
|
||||
task_id,
|
||||
model_events=request.model_events,
|
||||
)[0]
|
||||
|
||||
key = ScalarKeyEnum.iter
|
||||
@@ -353,7 +366,7 @@ def get_task_events(_, company_id, request: TaskEventsRequest):
|
||||
total = event_bll.events_iterator.count_task_events(
|
||||
event_type=request.event_type,
|
||||
company_id=task_or_model.get_index_company(),
|
||||
task_id=task_id,
|
||||
task_ids=[task_id],
|
||||
metric_variants=metric_variants,
|
||||
)
|
||||
|
||||
@@ -398,17 +411,18 @@ def get_scalar_metric_data(call, company_id, _):
|
||||
model_events = call.data.get("model_events", False)
|
||||
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=model_events,
|
||||
company_id,
|
||||
task_id,
|
||||
model_events=model_events,
|
||||
)[0]
|
||||
result = event_bll.get_task_events(
|
||||
task_or_model.get_index_company(),
|
||||
task_id,
|
||||
event_type=EventType.metrics_scalar,
|
||||
sort=[{"iter": {"order": "desc"}}],
|
||||
metric=metric,
|
||||
metrics={metric: []},
|
||||
scroll_id=scroll_id,
|
||||
no_scroll=no_scroll,
|
||||
model_events=model_events,
|
||||
)
|
||||
|
||||
call.result.data = dict(
|
||||
@@ -457,19 +471,23 @@ def scalar_metrics_iter_histogram(
|
||||
task_id=request.task,
|
||||
samples=request.samples,
|
||||
key=request.key,
|
||||
metric_variants=_get_metric_variants_from_request(request.metrics),
|
||||
)
|
||||
call.result.data = metrics
|
||||
|
||||
|
||||
def _get_task_or_model_index_company(
|
||||
company_id: str, task_ids: Sequence[str], model_events=False,
|
||||
) -> Tuple[str, Sequence[Task]]:
|
||||
def _get_task_or_model_index_companies(
|
||||
company_id: str,
|
||||
task_ids: Sequence[str],
|
||||
model_events=False,
|
||||
) -> TaskCompanies:
|
||||
"""
|
||||
Verify that all tasks exists and belong to store data in the same company index
|
||||
Return company and tasks
|
||||
Returns lists of tasks grouped by company
|
||||
"""
|
||||
tasks_or_models = _assert_task_or_model_exists(
|
||||
company_id, task_ids, model_events=model_events,
|
||||
company_id,
|
||||
task_ids,
|
||||
model_events=model_events,
|
||||
)
|
||||
|
||||
unique_ids = set(task_ids)
|
||||
@@ -482,13 +500,7 @@ def _get_task_or_model_index_company(
|
||||
)
|
||||
raise error_cls(company=company_id, ids=invalid)
|
||||
|
||||
companies = {t.get_index_company() for t in tasks_or_models}
|
||||
if len(companies) > 1:
|
||||
raise errors.bad_request.InvalidTaskId(
|
||||
"only tasks from the same company are supported"
|
||||
)
|
||||
|
||||
return companies.pop(), tasks_or_models
|
||||
return bucketize(tasks_or_models, key=lambda t: t.get_index_company())
|
||||
|
||||
|
||||
@endpoint(
|
||||
@@ -501,30 +513,44 @@ def multi_task_scalar_metrics_iter_histogram(
|
||||
task_ids = request.tasks
|
||||
if isinstance(task_ids, str):
|
||||
task_ids = [s.strip() for s in task_ids.split(",")]
|
||||
company, tasks_or_models = _get_task_or_model_index_company(
|
||||
company_id, task_ids, request.model_events
|
||||
)
|
||||
|
||||
call.result.data = dict(
|
||||
metrics=event_bll.metrics.compare_scalar_metrics_average_per_iter(
|
||||
company_id=company,
|
||||
tasks=tasks_or_models,
|
||||
companies=_get_task_or_model_index_companies(
|
||||
company_id, task_ids, request.model_events
|
||||
),
|
||||
samples=request.samples,
|
||||
key=request.key,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _get_single_value_metrics_response(
|
||||
companies: TaskCompanies, value_metrics: Mapping[str, dict]
|
||||
) -> Sequence[dict]:
|
||||
task_names = {
|
||||
task.id: task.name for task in itertools.chain.from_iterable(companies.values())
|
||||
}
|
||||
return [
|
||||
{"task": task_id, "task_name": task_names.get(task_id), "values": values}
|
||||
for task_id, values in value_metrics.items()
|
||||
]
|
||||
|
||||
|
||||
@endpoint("events.get_task_single_value_metrics")
|
||||
def get_task_single_value_metrics(
|
||||
call, company_id: str, request: SingleValueMetricsRequest
|
||||
):
|
||||
company, tasks_or_models = _get_task_or_model_index_company(
|
||||
companies = _get_task_or_model_index_companies(
|
||||
company_id, request.tasks, request.model_events
|
||||
)
|
||||
|
||||
res = event_bll.metrics.get_task_single_value_metrics(company, tasks_or_models)
|
||||
call.result.data = dict(
|
||||
tasks=[{"task": task, "values": values} for task, values in res.items()]
|
||||
tasks=_get_single_value_metrics_response(
|
||||
companies=companies,
|
||||
value_metrics=event_bll.metrics.get_task_single_value_metrics(
|
||||
companies=companies
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -534,22 +560,23 @@ def get_multi_task_plots_v1_7(call, company_id, _):
|
||||
iters = call.data.get("iters", 1)
|
||||
scroll_id = call.data.get("scroll_id")
|
||||
|
||||
company, tasks_or_models = _get_task_or_model_index_company(company_id, task_ids)
|
||||
companies = _get_task_or_model_index_companies(company_id, task_ids)
|
||||
|
||||
# Get last 10K events by iteration and group them by unique metric+variant, returning top events for combination
|
||||
result = event_bll.get_task_events(
|
||||
company,
|
||||
task_ids,
|
||||
company_id=list(companies),
|
||||
task_id=task_ids,
|
||||
event_type=EventType.metrics_plot,
|
||||
sort=[{"iter": {"order": "desc"}}],
|
||||
size=10000,
|
||||
scroll_id=scroll_id,
|
||||
)
|
||||
|
||||
tasks = {t.id: t.name for t in tasks_or_models}
|
||||
|
||||
task_names = {
|
||||
t.id: t.name for t in itertools.chain.from_iterable(companies.values())
|
||||
}
|
||||
return_events = _get_top_iter_unique_events_per_task(
|
||||
result.events, max_iters=iters, tasks=tasks
|
||||
result.events, max_iters=iters, task_names=task_names
|
||||
)
|
||||
|
||||
call.result.data = dict(
|
||||
@@ -560,40 +587,54 @@ def get_multi_task_plots_v1_7(call, company_id, _):
|
||||
)
|
||||
|
||||
|
||||
@endpoint("events.get_multi_task_plots", min_version="1.8", required_fields=["tasks"])
|
||||
def get_multi_task_plots(call, company_id, _):
|
||||
task_ids = call.data["tasks"]
|
||||
iters = call.data.get("iters", 1)
|
||||
scroll_id = call.data.get("scroll_id")
|
||||
no_scroll = call.data.get("no_scroll", False)
|
||||
model_events = call.data.get("model_events", False)
|
||||
|
||||
company, tasks_or_models = _get_task_or_model_index_company(
|
||||
company_id, task_ids, model_events
|
||||
)
|
||||
|
||||
def _get_multitask_plots(
|
||||
companies: TaskCompanies,
|
||||
last_iters: int,
|
||||
last_iters_per_task_metric: bool,
|
||||
metrics: MetricVariants = None,
|
||||
scroll_id=None,
|
||||
no_scroll=True,
|
||||
) -> Tuple[dict, int, str]:
|
||||
task_names = {
|
||||
t.id: t.name for t in itertools.chain.from_iterable(companies.values())
|
||||
}
|
||||
result = event_bll.get_task_events(
|
||||
company,
|
||||
task_ids,
|
||||
company_id=list(companies),
|
||||
task_id=list(task_names),
|
||||
event_type=EventType.metrics_plot,
|
||||
metrics=metrics,
|
||||
last_iter_count=last_iters,
|
||||
sort=[{"iter": {"order": "desc"}}],
|
||||
last_iter_count=iters,
|
||||
scroll_id=scroll_id,
|
||||
no_scroll=no_scroll,
|
||||
model_events=model_events,
|
||||
size=config.get(
|
||||
"services.events.events_retrieval.multi_plots_batch_size", 1000
|
||||
),
|
||||
last_iters_per_task_metric=last_iters_per_task_metric,
|
||||
)
|
||||
|
||||
tasks = {t.id: t.name for t in tasks_or_models}
|
||||
|
||||
return_events = _get_top_iter_unique_events_per_task(
|
||||
result.events, max_iters=iters, tasks=tasks
|
||||
result.events, max_iters=last_iters, task_names=task_names
|
||||
)
|
||||
return return_events, result.total_events, result.next_scroll_id
|
||||
|
||||
|
||||
@endpoint("events.get_multi_task_plots", min_version="1.8")
|
||||
def get_multi_task_plots(call, company_id, request: MultiTaskPlotsRequest):
|
||||
companies = _get_task_or_model_index_companies(
|
||||
company_id, request.tasks, model_events=request.model_events
|
||||
)
|
||||
return_events, total_events, next_scroll_id = _get_multitask_plots(
|
||||
companies=companies,
|
||||
last_iters=request.iters,
|
||||
scroll_id=request.scroll_id,
|
||||
no_scroll=request.no_scroll,
|
||||
last_iters_per_task_metric=request.last_iters_per_task_metric,
|
||||
)
|
||||
call.result.data = dict(
|
||||
plots=return_events,
|
||||
returned=len(return_events),
|
||||
total=result.total_events,
|
||||
scroll_id=result.next_scroll_id,
|
||||
total=total_events,
|
||||
scroll_id=next_scroll_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -648,20 +689,15 @@ def _get_metric_variants_from_request(
|
||||
def get_task_plots(call, company_id, request: TaskPlotsRequest):
|
||||
task_id = request.task
|
||||
iters = request.iters
|
||||
scroll_id = request.scroll_id
|
||||
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=request.model_events
|
||||
)[0]
|
||||
result = event_bll.get_task_plots(
|
||||
task_or_model.get_index_company(),
|
||||
tasks=[task_id],
|
||||
sort=[{"iter": {"order": "desc"}}],
|
||||
task_id=task_id,
|
||||
last_iterations_per_plot=iters,
|
||||
scroll_id=scroll_id,
|
||||
no_scroll=request.no_scroll,
|
||||
metric_variants=_get_metric_variants_from_request(request.metrics),
|
||||
model_events=request.model_events,
|
||||
)
|
||||
|
||||
return_events = result.events
|
||||
@@ -674,24 +710,43 @@ def get_task_plots(call, company_id, request: TaskPlotsRequest):
|
||||
)
|
||||
|
||||
|
||||
def _task_metrics_dict_from_request(req_metrics: Sequence[TaskMetric]) -> dict:
|
||||
task_metrics = defaultdict(dict)
|
||||
for tm in req_metrics:
|
||||
task_metrics[tm.task][tm.metric] = tm.variants
|
||||
for metrics in task_metrics.values():
|
||||
if None in metrics:
|
||||
metrics.clear()
|
||||
|
||||
return task_metrics
|
||||
|
||||
|
||||
def _get_metrics_response(metric_events: Sequence[tuple]) -> Sequence[MetricEvents]:
|
||||
return [
|
||||
MetricEvents(
|
||||
task=task,
|
||||
iterations=[
|
||||
IterationEvents(iter=iteration["iter"], events=iteration["events"])
|
||||
for iteration in iterations
|
||||
],
|
||||
)
|
||||
for (task, iterations) in metric_events
|
||||
]
|
||||
|
||||
|
||||
@endpoint(
|
||||
"events.plots",
|
||||
request_data_model=MetricEventsRequest,
|
||||
response_data_model=MetricEventsResponse,
|
||||
)
|
||||
def task_plots(call, company_id, request: MetricEventsRequest):
|
||||
task_metrics = defaultdict(dict)
|
||||
for tm in request.metrics:
|
||||
task_metrics[tm.task][tm.metric] = tm.variants
|
||||
for metrics in task_metrics.values():
|
||||
if None in metrics:
|
||||
metrics.clear()
|
||||
|
||||
company, _ = _get_task_or_model_index_company(
|
||||
company_id, task_ids=list(task_metrics), model_events=request.model_events
|
||||
task_metrics = _task_metrics_dict_from_request(request.metrics)
|
||||
task_ids = list(task_metrics)
|
||||
task_or_models = _assert_task_or_model_exists(
|
||||
company_id, task_ids=task_ids, model_events=request.model_events
|
||||
)
|
||||
result = event_bll.plots_iterator.get_task_events(
|
||||
company_id=company,
|
||||
companies={t.id: t.get_index_company() for t in task_or_models},
|
||||
task_metrics=task_metrics,
|
||||
iter_count=request.iters,
|
||||
navigate_earlier=request.navigate_earlier,
|
||||
@@ -701,16 +756,7 @@ def task_plots(call, company_id, request: MetricEventsRequest):
|
||||
|
||||
call.result.data_model = MetricEventsResponse(
|
||||
scroll_id=result.next_scroll_id,
|
||||
metrics=[
|
||||
MetricEvents(
|
||||
task=task,
|
||||
iterations=[
|
||||
IterationEvents(iter=iteration["iter"], events=iteration["events"])
|
||||
for iteration in iterations
|
||||
],
|
||||
)
|
||||
for (task, iterations) in result.metric_events
|
||||
],
|
||||
metrics=_get_metrics_response(result.metric_events),
|
||||
)
|
||||
|
||||
|
||||
@@ -759,7 +805,9 @@ def get_debug_images_v1_8(call, company_id, _):
|
||||
model_events = call.data.get("model_events", False)
|
||||
|
||||
tasks_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=model_events,
|
||||
company_id,
|
||||
task_id,
|
||||
model_events=model_events,
|
||||
)[0]
|
||||
result = event_bll.get_task_events(
|
||||
tasks_or_model.get_index_company(),
|
||||
@@ -768,7 +816,6 @@ def get_debug_images_v1_8(call, company_id, _):
|
||||
sort=[{"iter": {"order": "desc"}}],
|
||||
last_iter_count=iters,
|
||||
scroll_id=scroll_id,
|
||||
model_events=model_events,
|
||||
)
|
||||
|
||||
return_events = result.events
|
||||
@@ -789,19 +836,13 @@ def get_debug_images_v1_8(call, company_id, _):
|
||||
response_data_model=MetricEventsResponse,
|
||||
)
|
||||
def get_debug_images(call, company_id, request: MetricEventsRequest):
|
||||
task_metrics = defaultdict(dict)
|
||||
for tm in request.metrics:
|
||||
task_metrics[tm.task][tm.metric] = tm.variants
|
||||
for metrics in task_metrics.values():
|
||||
if None in metrics:
|
||||
metrics.clear()
|
||||
|
||||
company, _ = _get_task_or_model_index_company(
|
||||
company_id, task_ids=list(task_metrics), model_events=request.model_events
|
||||
task_metrics = _task_metrics_dict_from_request(request.metrics)
|
||||
task_ids = list(task_metrics)
|
||||
task_or_models = _assert_task_or_model_exists(
|
||||
company_id, task_ids=task_ids, model_events=request.model_events
|
||||
)
|
||||
|
||||
result = event_bll.debug_images_iterator.get_task_events(
|
||||
company_id=company,
|
||||
companies={t.id: t.get_index_company() for t in task_or_models},
|
||||
task_metrics=task_metrics,
|
||||
iter_count=request.iters,
|
||||
navigate_earlier=request.navigate_earlier,
|
||||
@@ -811,16 +852,7 @@ def get_debug_images(call, company_id, request: MetricEventsRequest):
|
||||
|
||||
call.result.data_model = MetricEventsResponse(
|
||||
scroll_id=result.next_scroll_id,
|
||||
metrics=[
|
||||
MetricEvents(
|
||||
task=task,
|
||||
iterations=[
|
||||
IterationEvents(iter=iteration["iter"], events=iteration["events"])
|
||||
for iteration in iterations
|
||||
],
|
||||
)
|
||||
for (task, iterations) in result.metric_events
|
||||
],
|
||||
metrics=_get_metrics_response(result.metric_events),
|
||||
)
|
||||
|
||||
|
||||
@@ -831,7 +863,9 @@ def get_debug_images(call, company_id, request: MetricEventsRequest):
|
||||
)
|
||||
def get_debug_image_sample(call, company_id, request: GetVariantSampleRequest):
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, request.task, model_events=request.model_events,
|
||||
company_id,
|
||||
request.task,
|
||||
model_events=request.model_events,
|
||||
)[0]
|
||||
res = event_bll.debug_image_sample_history.get_sample_for_variant(
|
||||
company_id=task_or_model.get_index_company(),
|
||||
@@ -853,7 +887,9 @@ def get_debug_image_sample(call, company_id, request: GetVariantSampleRequest):
|
||||
)
|
||||
def next_debug_image_sample(call, company_id, request: NextHistorySampleRequest):
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, request.task, model_events=request.model_events,
|
||||
company_id,
|
||||
request.task,
|
||||
model_events=request.model_events,
|
||||
)[0]
|
||||
res = event_bll.debug_image_sample_history.get_next_sample(
|
||||
company_id=task_or_model.get_index_company(),
|
||||
@@ -866,11 +902,14 @@ def next_debug_image_sample(call, company_id, request: NextHistorySampleRequest)
|
||||
|
||||
|
||||
@endpoint(
|
||||
"events.get_plot_sample", request_data_model=GetMetricSamplesRequest,
|
||||
"events.get_plot_sample",
|
||||
request_data_model=GetMetricSamplesRequest,
|
||||
)
|
||||
def get_plot_sample(call, company_id, request: GetMetricSamplesRequest):
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, request.task, model_events=request.model_events,
|
||||
company_id,
|
||||
request.task,
|
||||
model_events=request.model_events,
|
||||
)[0]
|
||||
res = event_bll.plot_sample_history.get_samples_for_metric(
|
||||
company_id=task_or_model.get_index_company(),
|
||||
@@ -885,11 +924,14 @@ def get_plot_sample(call, company_id, request: GetMetricSamplesRequest):
|
||||
|
||||
|
||||
@endpoint(
|
||||
"events.next_plot_sample", request_data_model=NextHistorySampleRequest,
|
||||
"events.next_plot_sample",
|
||||
request_data_model=NextHistorySampleRequest,
|
||||
)
|
||||
def next_plot_sample(call, company_id, request: NextHistorySampleRequest):
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, request.task, model_events=request.model_events,
|
||||
company_id,
|
||||
request.task,
|
||||
model_events=request.model_events,
|
||||
)[0]
|
||||
res = event_bll.plot_sample_history.get_next_sample(
|
||||
company_id=task_or_model.get_index_company(),
|
||||
@@ -903,11 +945,15 @@ def next_plot_sample(call, company_id, request: NextHistorySampleRequest):
|
||||
|
||||
@endpoint("events.get_task_metrics", request_data_model=TaskMetricsRequest)
|
||||
def get_task_metrics(call: APICall, company_id, request: TaskMetricsRequest):
|
||||
company, _ = _get_task_or_model_index_company(
|
||||
company_id, request.tasks, model_events=request.model_events
|
||||
task_or_models = _assert_task_or_model_exists(
|
||||
company_id,
|
||||
request.tasks,
|
||||
model_events=request.model_events,
|
||||
)
|
||||
res = event_bll.metrics.get_task_metrics(
|
||||
company, task_ids=request.tasks, event_type=request.event_type
|
||||
task_or_models[0].get_index_company(),
|
||||
task_ids=request.tasks,
|
||||
event_type=request.event_type,
|
||||
)
|
||||
call.result.data = {
|
||||
"metrics": [{"task": task, "metrics": metrics} for (task, metrics) in res]
|
||||
@@ -955,20 +1001,22 @@ def clear_task_log(call: APICall, company_id: str, request: ClearTaskLogRequest)
|
||||
)
|
||||
|
||||
|
||||
def _get_top_iter_unique_events_per_task(events, max_iters, tasks):
|
||||
key = itemgetter("metric", "variant", "task", "iter")
|
||||
|
||||
def _get_top_iter_unique_events_per_task(
|
||||
events, max_iters: int, task_names: Mapping[str, str]
|
||||
):
|
||||
key_fields = ("metric", "variant", "task")
|
||||
unique_events = itertools.chain.from_iterable(
|
||||
itertools.islice(group, max_iters)
|
||||
for _, group in itertools.groupby(
|
||||
sorted(events, key=key, reverse=True), key=key
|
||||
sorted(events, key=itemgetter(*(key_fields + ("iter",))), reverse=True),
|
||||
key=itemgetter(*key_fields),
|
||||
)
|
||||
)
|
||||
|
||||
def collect(evs, fields):
|
||||
if not fields:
|
||||
evs = list(evs)
|
||||
return {"name": tasks.get(evs[0].get("task")), "plots": evs}
|
||||
return {"name": task_names.get(evs[0].get("task")), "plots": evs}
|
||||
return {
|
||||
str(k): collect(group, fields[1:])
|
||||
for k, group in itertools.groupby(evs, key=itemgetter(fields[0]))
|
||||
@@ -1034,14 +1082,16 @@ def scalar_metrics_iter_raw(
|
||||
request.batch_size = request.batch_size or scroll.request.batch_size
|
||||
|
||||
task_id = request.task
|
||||
task_or_model = _assert_task_or_model_exists(company_id, task_id, model_events=request.model_events)[0]
|
||||
task_or_model = _assert_task_or_model_exists(
|
||||
company_id, task_id, model_events=request.model_events
|
||||
)[0]
|
||||
metric_variants = _get_metric_variants_from_request([request.metric])
|
||||
|
||||
if request.count_total and total is None:
|
||||
total = event_bll.events_iterator.count_task_events(
|
||||
event_type=EventType.metrics_scalar,
|
||||
company_id=task_or_model.get_index_company(),
|
||||
task_id=task_id,
|
||||
task_ids=[task_id],
|
||||
metric_variants=metric_variants,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Sequence
|
||||
from typing import Sequence, Union
|
||||
|
||||
from mongoengine import Q, EmbeddedDocument
|
||||
|
||||
@@ -22,6 +22,7 @@ from apiserver.apimodels.models import (
|
||||
ModelsDeleteManyRequest,
|
||||
ModelsGetRequest,
|
||||
)
|
||||
from apiserver.apimodels.tasks import UpdateTagsRequest
|
||||
from apiserver.bll.model import ModelBLL, Metadata
|
||||
from apiserver.bll.organization import OrgBLL, Tags
|
||||
from apiserver.bll.project import ProjectBLL
|
||||
@@ -59,23 +60,28 @@ org_bll = OrgBLL()
|
||||
project_bll = ProjectBLL()
|
||||
|
||||
|
||||
def conform_model_data(call: APICall, model_data: Union[Sequence[dict], dict]):
|
||||
conform_output_tags(call, model_data)
|
||||
unescape_metadata(call, model_data)
|
||||
|
||||
|
||||
@endpoint("models.get_by_id", required_fields=["model"])
|
||||
def get_by_id(call: APICall, company_id, _):
|
||||
model_id = call.data["model"]
|
||||
|
||||
Metadata.escape_query_parameters(call)
|
||||
call_data = Metadata.escape_query_parameters(call.data)
|
||||
models = Model.get_many(
|
||||
company=company_id,
|
||||
query_dict=call.data,
|
||||
query_dict=call_data,
|
||||
query=Q(id=model_id),
|
||||
allow_public=True,
|
||||
)
|
||||
if not models:
|
||||
raise errors.bad_request.InvalidModelId(
|
||||
"no such public or company model", id=model_id, company=company_id,
|
||||
"no such public or company model",
|
||||
id=model_id,
|
||||
company=company_id,
|
||||
)
|
||||
conform_output_tags(call, models[0])
|
||||
unescape_metadata(call, models[0])
|
||||
conform_model_data(call, models[0])
|
||||
call.result.data = {"model": models[0]}
|
||||
|
||||
|
||||
@@ -99,35 +105,38 @@ def get_by_task_id(call: APICall, company_id, _):
|
||||
).first()
|
||||
if not model:
|
||||
raise errors.bad_request.InvalidModelId(
|
||||
"no such public or company model", id=model_id, company=company_id,
|
||||
"no such public or company model",
|
||||
id=model_id,
|
||||
company=company_id,
|
||||
)
|
||||
model_dict = model.to_proper_dict()
|
||||
conform_output_tags(call, model_dict)
|
||||
unescape_metadata(call, model_dict)
|
||||
conform_model_data(call, model_dict)
|
||||
call.result.data = {"model": model_dict}
|
||||
|
||||
|
||||
@endpoint("models.get_all_ex", request_data_model=ModelsGetRequest)
|
||||
def get_all_ex(call: APICall, company_id, request: ModelsGetRequest):
|
||||
conform_tag_fields(call, call.data)
|
||||
process_include_subprojects(call.data)
|
||||
Metadata.escape_query_parameters(call)
|
||||
call_data = Metadata.escape_query_parameters(call.data)
|
||||
process_include_subprojects(call_data)
|
||||
ret_params = {}
|
||||
models = Model.get_many_with_join(
|
||||
company=company_id,
|
||||
query_dict=call.data,
|
||||
allow_public=True,
|
||||
query_dict=call_data,
|
||||
allow_public=request.allow_public,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
conform_output_tags(call, models)
|
||||
unescape_metadata(call, models)
|
||||
conform_model_data(call, models)
|
||||
|
||||
if not request.include_stats:
|
||||
call.result.data = {"models": models, **ret_params}
|
||||
return
|
||||
|
||||
model_ids = {model["id"] for model in models}
|
||||
stats = ModelBLL.get_model_stats(company=company_id, model_ids=list(model_ids),)
|
||||
stats = ModelBLL.get_model_stats(
|
||||
company=company_id,
|
||||
model_ids=list(model_ids),
|
||||
)
|
||||
|
||||
for model in models:
|
||||
model["stats"] = stats.get(model["id"])
|
||||
@@ -138,29 +147,28 @@ def get_all_ex(call: APICall, company_id, request: ModelsGetRequest):
|
||||
@endpoint("models.get_by_id_ex", required_fields=["id"])
|
||||
def get_by_id_ex(call: APICall, company_id, _):
|
||||
conform_tag_fields(call, call.data)
|
||||
Metadata.escape_query_parameters(call)
|
||||
call_data = Metadata.escape_query_parameters(call.data)
|
||||
models = Model.get_many_with_join(
|
||||
company=company_id, query_dict=call.data, allow_public=True
|
||||
company=company_id, query_dict=call_data, allow_public=True
|
||||
)
|
||||
conform_output_tags(call, models)
|
||||
unescape_metadata(call, models)
|
||||
conform_model_data(call, models)
|
||||
call.result.data = {"models": models}
|
||||
|
||||
|
||||
@endpoint("models.get_all", required_fields=[])
|
||||
def get_all(call: APICall, company_id, _):
|
||||
conform_tag_fields(call, call.data)
|
||||
Metadata.escape_query_parameters(call)
|
||||
call_data = Metadata.escape_query_parameters(call.data)
|
||||
process_include_subprojects(call_data)
|
||||
ret_params = {}
|
||||
models = Model.get_many(
|
||||
company=company_id,
|
||||
parameters=call.data,
|
||||
query_dict=call.data,
|
||||
parameters=call_data,
|
||||
query_dict=call_data,
|
||||
allow_public=True,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
conform_output_tags(call, models)
|
||||
unescape_metadata(call, models)
|
||||
conform_model_data(call, models)
|
||||
call.result.data = {"models": models, **ret_params}
|
||||
|
||||
|
||||
@@ -212,7 +220,7 @@ def _update_cached_tags(company: str, project: str, fields: dict):
|
||||
org_bll.update_tags(
|
||||
company,
|
||||
Tags.Model,
|
||||
project=project,
|
||||
projects=[project],
|
||||
tags=fields.get("tags"),
|
||||
system_tags=fields.get("system_tags"),
|
||||
)
|
||||
@@ -220,7 +228,9 @@ def _update_cached_tags(company: str, project: str, fields: dict):
|
||||
|
||||
def _reset_cached_tags(company: str, projects: Sequence[str]):
|
||||
org_bll.reset_tags(
|
||||
company, Tags.Model, projects=projects,
|
||||
company,
|
||||
Tags.Model,
|
||||
projects=projects,
|
||||
)
|
||||
|
||||
|
||||
@@ -283,6 +293,8 @@ def update_for_task(call: APICall, company_id, _):
|
||||
id=database.utils.id(),
|
||||
created=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=call.identity.user,
|
||||
user=call.identity.user,
|
||||
company=company_id,
|
||||
project=task.project,
|
||||
@@ -301,6 +313,7 @@ def update_for_task(call: APICall, company_id, _):
|
||||
TaskBLL.update_statistics(
|
||||
task_id=task_id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
last_iteration_max=iteration,
|
||||
models__output=[
|
||||
ModelItem(
|
||||
@@ -320,7 +333,6 @@ def update_for_task(call: APICall, company_id, _):
|
||||
response_data_model=CreateModelResponse,
|
||||
)
|
||||
def create(call: APICall, company_id, req_model: CreateModelRequest):
|
||||
|
||||
if req_model.public:
|
||||
company_id = ""
|
||||
|
||||
@@ -345,6 +357,8 @@ def create(call: APICall, company_id, req_model: CreateModelRequest):
|
||||
company=company_id,
|
||||
created=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=call.identity.user,
|
||||
**fields,
|
||||
)
|
||||
model.save()
|
||||
@@ -414,12 +428,20 @@ def edit(call: APICall, company_id, _):
|
||||
task_id = model.task or fields.get("task")
|
||||
if task_id and iteration is not None:
|
||||
TaskBLL.update_statistics(
|
||||
task_id=task_id, company_id=company_id, last_iteration_max=iteration,
|
||||
task_id=task_id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
last_iteration_max=iteration,
|
||||
)
|
||||
|
||||
if fields:
|
||||
now = datetime.utcnow()
|
||||
fields.update(
|
||||
last_change=now,
|
||||
last_changed_by=call.identity.user,
|
||||
)
|
||||
if any(uf in fields for uf in last_update_fields):
|
||||
fields.update(last_update=datetime.utcnow())
|
||||
fields.update(last_update=now)
|
||||
|
||||
updated = model.update(upsert=False, **fields)
|
||||
if updated:
|
||||
@@ -428,8 +450,7 @@ def edit(call: APICall, company_id, _):
|
||||
_reset_cached_tags(company_id, projects=[new_project, model.project])
|
||||
else:
|
||||
_update_cached_tags(company_id, project=model.project, fields=fields)
|
||||
conform_output_tags(call, fields)
|
||||
unescape_metadata(call, fields)
|
||||
conform_model_data(call, fields)
|
||||
call.result.data_model = UpdateResponse(updated=updated, fields=fields)
|
||||
else:
|
||||
call.result.data_model = UpdateResponse(updated=0)
|
||||
@@ -446,13 +467,25 @@ def _update_model(call: APICall, company_id, model_id=None):
|
||||
iteration = data.get("iteration")
|
||||
if task_id and iteration is not None:
|
||||
TaskBLL.update_statistics(
|
||||
task_id=task_id, company_id=company_id, last_iteration_max=iteration,
|
||||
task_id=task_id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
last_iteration_max=iteration,
|
||||
)
|
||||
|
||||
updated_count, updated_fields = Model.safe_update(company_id, model.id, data)
|
||||
now = datetime.utcnow()
|
||||
updated_count, updated_fields = Model.safe_update(
|
||||
company_id,
|
||||
model.id,
|
||||
data,
|
||||
injected_update=dict(
|
||||
last_change=now,
|
||||
last_changed_by=call.identity.user,
|
||||
),
|
||||
)
|
||||
if updated_count:
|
||||
if any(uf in updated_fields for uf in last_update_fields):
|
||||
model.update(upsert=False, last_update=datetime.utcnow())
|
||||
model.update(upsert=False, last_update=now)
|
||||
|
||||
new_project = updated_fields.get("project", model.project)
|
||||
if new_project != model.project:
|
||||
@@ -461,8 +494,7 @@ def _update_model(call: APICall, company_id, model_id=None):
|
||||
_update_cached_tags(
|
||||
company_id, project=model.project, fields=updated_fields
|
||||
)
|
||||
conform_output_tags(call, updated_fields)
|
||||
unescape_metadata(call, updated_fields)
|
||||
conform_model_data(call, updated_fields)
|
||||
return UpdateResponse(updated=updated_count, fields=updated_fields)
|
||||
|
||||
|
||||
@@ -482,6 +514,7 @@ def set_ready(call: APICall, company_id: str, request: PublishModelRequest):
|
||||
updated, published_task = ModelBLL.publish_model(
|
||||
model_id=request.model,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force_publish_task=request.force_publish_task,
|
||||
publish_task_func=publish_task if request.publish_task else None,
|
||||
)
|
||||
@@ -500,6 +533,7 @@ def publish_many(call: APICall, company_id, request: ModelsPublishManyRequest):
|
||||
func=partial(
|
||||
ModelBLL.publish_model,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force_publish_task=request.force_publish_task,
|
||||
publish_task_func=publish_task if request.publish_task else None,
|
||||
),
|
||||
@@ -522,7 +556,11 @@ def publish_many(call: APICall, company_id, request: ModelsPublishManyRequest):
|
||||
@endpoint("models.delete", request_data_model=DeleteModelRequest)
|
||||
def delete(call: APICall, company_id, request: DeleteModelRequest):
|
||||
del_count, model = ModelBLL.delete_model(
|
||||
model_id=request.model, company_id=company_id, force=request.force
|
||||
model_id=request.model,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force=request.force,
|
||||
delete_external_artifacts=request.delete_external_artifacts,
|
||||
)
|
||||
if del_count:
|
||||
_reset_cached_tags(
|
||||
@@ -539,7 +577,13 @@ def delete(call: APICall, company_id, request: DeleteModelRequest):
|
||||
)
|
||||
def delete(call: APICall, company_id, request: ModelsDeleteManyRequest):
|
||||
results, failures = run_batch_operation(
|
||||
func=partial(ModelBLL.delete_model, company_id=company_id, force=request.force),
|
||||
func=partial(
|
||||
ModelBLL.delete_model,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force=request.force,
|
||||
delete_external_artifacts=request.delete_external_artifacts,
|
||||
),
|
||||
ids=request.ids,
|
||||
)
|
||||
|
||||
@@ -563,7 +607,10 @@ def delete(call: APICall, company_id, request: ModelsDeleteManyRequest):
|
||||
)
|
||||
def archive_many(call: APICall, company_id, request: BatchRequest):
|
||||
results, failures = run_batch_operation(
|
||||
func=partial(ModelBLL.archive_model, company_id=company_id), ids=request.ids,
|
||||
func=partial(
|
||||
ModelBLL.archive_model, company_id=company_id, user_id=call.identity.user
|
||||
),
|
||||
ids=request.ids,
|
||||
)
|
||||
call.result.data_model = BatchResponse(
|
||||
succeeded=[dict(id=_id, archived=bool(archived)) for _id, archived in results],
|
||||
@@ -578,7 +625,8 @@ def archive_many(call: APICall, company_id, request: BatchRequest):
|
||||
)
|
||||
def unarchive_many(call: APICall, company_id, request: BatchRequest):
|
||||
results, failures = run_batch_operation(
|
||||
func=partial(ModelBLL.unarchive_model, company_id=company_id), ids=request.ids,
|
||||
func=partial(ModelBLL.unarchive_model, company_id=company_id, user_id=call.identity.user),
|
||||
ids=request.ids,
|
||||
)
|
||||
call.result.data_model = BatchResponse(
|
||||
succeeded=[
|
||||
@@ -591,7 +639,11 @@ def unarchive_many(call: APICall, company_id, request: BatchRequest):
|
||||
@endpoint("models.make_public", min_version="2.9", request_data_model=MakePublicRequest)
|
||||
def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
call.result.data = Model.set_public(
|
||||
company_id, ids=request.ids, invalid_cls=InvalidModelId, enabled=True
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
ids=request.ids,
|
||||
invalid_cls=InvalidModelId,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -600,13 +652,17 @@ def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
)
|
||||
def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
call.result.data = Model.set_public(
|
||||
company_id, request.ids, invalid_cls=InvalidModelId, enabled=False
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
ids=request.ids,
|
||||
invalid_cls=InvalidModelId,
|
||||
enabled=False,
|
||||
)
|
||||
|
||||
|
||||
@endpoint("models.move", request_data_model=MoveRequest)
|
||||
def move(call: APICall, company_id: str, request: MoveRequest):
|
||||
if not (request.project or request.project_name):
|
||||
if not ("project" in call.data or request.project_name):
|
||||
raise errors.bad_request.MissingRequiredFields(
|
||||
"project or project_name is required"
|
||||
)
|
||||
@@ -623,30 +679,51 @@ def move(call: APICall, company_id: str, request: MoveRequest):
|
||||
}
|
||||
|
||||
|
||||
@endpoint("models.update_tags")
|
||||
def update_tags(_, company_id: str, request: UpdateTagsRequest):
|
||||
return {
|
||||
"updated": org_bll.edit_entity_tags(
|
||||
company_id=company_id,
|
||||
entity_cls=Model,
|
||||
entity_ids=request.ids,
|
||||
add_tags=request.add_tags,
|
||||
remove_tags=request.remove_tags,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@endpoint("models.add_or_update_metadata", min_version="2.13")
|
||||
def add_or_update_metadata(
|
||||
_: APICall, company_id: str, request: AddOrUpdateMetadataRequest
|
||||
call: APICall, company_id: str, request: AddOrUpdateMetadataRequest
|
||||
):
|
||||
model_id = request.model
|
||||
model = ModelBLL.get_company_model_by_id(company_id=company_id, model_id=model_id)
|
||||
now = datetime.utcnow()
|
||||
return {
|
||||
"updated": Metadata.edit_metadata(
|
||||
model,
|
||||
items=request.metadata,
|
||||
replace_metadata=request.replace_metadata,
|
||||
last_update=datetime.utcnow(),
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=call.identity.user,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@endpoint("models.delete_metadata", min_version="2.13")
|
||||
def delete_metadata(_: APICall, company_id: str, request: DeleteMetadataRequest):
|
||||
def delete_metadata(call: APICall, company_id: str, request: DeleteMetadataRequest):
|
||||
model_id = request.model
|
||||
model = ModelBLL.get_company_model_by_id(
|
||||
company_id=company_id, model_id=model_id, only_fields=("id",)
|
||||
)
|
||||
now = datetime.utcnow()
|
||||
return {
|
||||
"updated": Metadata.delete_metadata(
|
||||
model, keys=request.keys, last_update=datetime.utcnow()
|
||||
model,
|
||||
keys=request.keys,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
last_changed_by=call.identity.user,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
import csv
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from io import StringIO
|
||||
from operator import itemgetter
|
||||
from typing import Mapping, Type
|
||||
from typing import Mapping, Type, Sequence, Optional, Callable, Hashable
|
||||
|
||||
from flask import stream_with_context
|
||||
from mongoengine import Q
|
||||
|
||||
from apiserver.apimodels.organization import TagsRequest, EntitiesCountRequest
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels.organization import (
|
||||
TagsRequest,
|
||||
EntitiesCountRequest,
|
||||
DownloadForGetAllRequest,
|
||||
EntityType,
|
||||
PrepareDownloadForGetAllRequest,
|
||||
)
|
||||
from apiserver.bll.model import Metadata
|
||||
from apiserver.bll.organization import OrgBLL, Tags
|
||||
from apiserver.bll.project import ProjectBLL
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model import User, AttributedDocument, EntityVisibility
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.database.model.task.task import Task, TaskType
|
||||
from apiserver.redis_manager import redman
|
||||
from apiserver.service_repo import endpoint, APICall
|
||||
from apiserver.services.models import conform_model_data
|
||||
from apiserver.services.tasks import (
|
||||
escape_execution_parameters,
|
||||
_hidden_query,
|
||||
conform_task_data,
|
||||
)
|
||||
from apiserver.services.utils import get_tags_filter_dictionary, sort_tags_response
|
||||
from apiserver.utilities import json
|
||||
from apiserver.utilities.dicts import nested_get
|
||||
|
||||
org_bll = OrgBLL()
|
||||
project_bll = ProjectBLL()
|
||||
redis = redman.connection("apiserver")
|
||||
conf = config.get("services.organization")
|
||||
|
||||
|
||||
@endpoint("organization.get_tags", request_data_model=TagsRequest)
|
||||
@@ -24,7 +48,10 @@ def get_tags(call: APICall, company, request: TagsRequest):
|
||||
ret = defaultdict(set)
|
||||
for entity in Tags.Model, Tags.Task:
|
||||
tags = org_bll.get_tags(
|
||||
company, entity, include_system=request.include_system, filter_=filter_dict,
|
||||
company,
|
||||
entity,
|
||||
include_system=request.include_system,
|
||||
filter_=filter_dict,
|
||||
)
|
||||
for field, vals in tags.items():
|
||||
ret[field] |= vals
|
||||
@@ -59,6 +86,7 @@ def get_entities_count(call: APICall, company, request: EntitiesCountRequest):
|
||||
"models": Model,
|
||||
"pipelines": Project,
|
||||
"datasets": Project,
|
||||
"reports": Task,
|
||||
}
|
||||
ret = {}
|
||||
for field, entity_cls in entity_classes.items():
|
||||
@@ -66,16 +94,20 @@ def get_entities_count(call: APICall, company, request: EntitiesCountRequest):
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
if field == "reports":
|
||||
data["type"] = TaskType.report
|
||||
data["include_subprojects"] = True
|
||||
|
||||
if request.active_users:
|
||||
if entity_cls is Project:
|
||||
requested_ids = data.get("id")
|
||||
if isinstance(requested_ids, str):
|
||||
requested_ids = [requested_ids]
|
||||
ids, _ = project_bll.get_projects_with_active_user(
|
||||
ids, _ = project_bll.get_projects_with_selected_children(
|
||||
company=company,
|
||||
users=request.active_users,
|
||||
project_ids=requested_ids,
|
||||
allow_public=True,
|
||||
allow_public=request.allow_public,
|
||||
)
|
||||
if not ids:
|
||||
ret[field] = 0
|
||||
@@ -85,11 +117,218 @@ def get_entities_count(call: APICall, company, request: EntitiesCountRequest):
|
||||
data["user"] = request.active_users
|
||||
|
||||
query = Q()
|
||||
if entity_cls in (Project, Task) and not request.search_hidden:
|
||||
if (
|
||||
entity_cls in (Project, Task)
|
||||
and field not in ("reports", "pipelines", "datasets")
|
||||
and not request.search_hidden
|
||||
):
|
||||
query &= Q(system_tags__ne=EntityVisibility.hidden.value)
|
||||
|
||||
ret[field] = entity_cls.get_count(
|
||||
company=company, query_dict=data, query=query, allow_public=True,
|
||||
company=company,
|
||||
query_dict=data,
|
||||
query=query,
|
||||
allow_public=request.allow_public,
|
||||
)
|
||||
|
||||
call.result.data = ret
|
||||
|
||||
|
||||
def _get_download_getter_fn(
|
||||
company: str,
|
||||
call: APICall,
|
||||
call_data: dict,
|
||||
allow_public: bool,
|
||||
entity_type: EntityType,
|
||||
) -> Optional[Callable[[int, int], Sequence[dict]]]:
|
||||
def get_task_data() -> Sequence[dict]:
|
||||
tasks = Task.get_many_with_join(
|
||||
company=company,
|
||||
query_dict=call_data,
|
||||
query=_hidden_query(call_data),
|
||||
allow_public=allow_public,
|
||||
)
|
||||
conform_task_data(call, tasks)
|
||||
return tasks
|
||||
|
||||
def get_model_data() -> Sequence[dict]:
|
||||
models = Model.get_many_with_join(
|
||||
company=company,
|
||||
query_dict=call_data,
|
||||
allow_public=allow_public,
|
||||
)
|
||||
conform_model_data(call, models)
|
||||
return models
|
||||
|
||||
if entity_type == EntityType.task:
|
||||
call_data = escape_execution_parameters(call_data)
|
||||
get_fn = get_task_data
|
||||
elif entity_type == EntityType.model:
|
||||
call_data = Metadata.escape_query_parameters(call_data)
|
||||
get_fn = get_model_data
|
||||
else:
|
||||
raise errors.bad_request.ValidationError(
|
||||
f"Unsupported entity type: {str(entity_type)}"
|
||||
)
|
||||
|
||||
def getter(page: int, page_size: int) -> Sequence[dict]:
|
||||
call_data.pop("scroll_id", None)
|
||||
call_data.pop("start", None)
|
||||
call_data.pop("size", None)
|
||||
call_data.pop("refresh_scroll", None)
|
||||
call_data["page"] = page
|
||||
call_data["page_size"] = page_size
|
||||
return get_fn()
|
||||
|
||||
return getter
|
||||
|
||||
|
||||
@endpoint("organization.prepare_download_for_get_all")
|
||||
def prepare_download_for_get_all(
|
||||
call: APICall, company: str, request: PrepareDownloadForGetAllRequest
|
||||
):
|
||||
# validate input params
|
||||
field_names = set()
|
||||
for fm in request.field_mappings:
|
||||
name = fm.name or fm.field
|
||||
if name in field_names:
|
||||
raise errors.bad_request.ValidationError(
|
||||
f"Field_name appears more than once in field_mappings: {str(name)}"
|
||||
)
|
||||
field_names.add(name)
|
||||
if fm.values:
|
||||
value_keys = set()
|
||||
for v in fm.values:
|
||||
if v.key in value_keys:
|
||||
raise errors.bad_request.ValidationError(
|
||||
f"Value key appears more than once in field_mappings: {str(v.key)}"
|
||||
)
|
||||
value_keys.add(v.key)
|
||||
|
||||
getter = _get_download_getter_fn(
|
||||
company,
|
||||
call,
|
||||
call_data=call.data.copy(),
|
||||
allow_public=request.allow_public,
|
||||
entity_type=request.entity_type,
|
||||
)
|
||||
# retrieve one element just to make sure that there are no issues with the call parameters
|
||||
if getter:
|
||||
getter(0, 1)
|
||||
|
||||
redis.setex(
|
||||
f"get_all_download_{call.id}",
|
||||
int(conf.get("download.redis_timeout_sec", 300)),
|
||||
json.dumps(call.data),
|
||||
)
|
||||
|
||||
call.result.data = dict(prepare_id=call.id)
|
||||
|
||||
|
||||
@endpoint("organization.download_for_get_all")
|
||||
def download_for_get_all(call: APICall, company, request: DownloadForGetAllRequest):
|
||||
request_data = redis.get(f"get_all_download_{request.prepare_id}")
|
||||
if not request_data:
|
||||
raise errors.bad_request.InvalidId(
|
||||
f"prepare ID not found", prepare_id=request.prepare_id
|
||||
)
|
||||
|
||||
try:
|
||||
call_data = json.loads(request_data)
|
||||
request = PrepareDownloadForGetAllRequest(**call_data)
|
||||
except Exception as ex:
|
||||
raise errors.server_error.DataError("failed parsing prepared data", ex=ex)
|
||||
|
||||
class SingleLine:
|
||||
@staticmethod
|
||||
def write(line: str) -> str:
|
||||
return line
|
||||
|
||||
def generate():
|
||||
field_mappings = {
|
||||
mapping.get("name", mapping["field"]): {
|
||||
"field_path": mapping["field"].split("."),
|
||||
"values": {
|
||||
v.get("key"): v.get("value")
|
||||
for v in (mapping.get("values") or [])
|
||||
},
|
||||
}
|
||||
for mapping in call_data.get("field_mappings", [])
|
||||
}
|
||||
get_fn = _get_download_getter_fn(
|
||||
company,
|
||||
call,
|
||||
call_data=call_data,
|
||||
allow_public=request.allow_public,
|
||||
entity_type=request.entity_type,
|
||||
)
|
||||
if not get_fn:
|
||||
yield csv.writer(SingleLine()).writerow(field_mappings)
|
||||
return
|
||||
|
||||
def get_entity_field_as_str(
|
||||
data: dict, field_path: Sequence[str], values: Mapping
|
||||
) -> str:
|
||||
val = nested_get(data, field_path, "")
|
||||
if isinstance(val, dict):
|
||||
val = val.get("id", "")
|
||||
if values and isinstance(val, Hashable):
|
||||
val = values.get(val, val)
|
||||
|
||||
return str(val)
|
||||
|
||||
def get_projected_fields(data: dict) -> Sequence[str]:
|
||||
return [
|
||||
get_entity_field_as_str(
|
||||
data, field_path=m["field_path"], values=m["values"]
|
||||
)
|
||||
for m in field_mappings.values()
|
||||
]
|
||||
|
||||
with ThreadPoolExecutor(1) as pool:
|
||||
page = 0
|
||||
page_size = int(conf.get("download.batch_size", 500))
|
||||
items_left = int(conf.get("download.max_download_items", 1000))
|
||||
future = pool.submit(get_fn, page, min(page_size, items_left))
|
||||
while items_left > 0:
|
||||
result = future.result()
|
||||
if not result:
|
||||
break
|
||||
|
||||
items_left -= len(result)
|
||||
page += 1
|
||||
if items_left > 0:
|
||||
future = pool.submit(get_fn, page, min(page_size, items_left))
|
||||
|
||||
with StringIO() as fp:
|
||||
writer = csv.writer(fp)
|
||||
if page == 1:
|
||||
fp.write("\ufeff") # utf-8 signature
|
||||
writer.writerow(field_mappings)
|
||||
writer.writerows(get_projected_fields(r) for r in result)
|
||||
yield fp.getvalue()
|
||||
|
||||
if page == 0:
|
||||
yield csv.writer(SingleLine()).writerow(field_mappings)
|
||||
|
||||
def get_project_name() -> Optional[str]:
|
||||
projects = call_data.get("project")
|
||||
if not projects or not isinstance(projects, (list, str)):
|
||||
return
|
||||
if isinstance(projects, list):
|
||||
if len(projects) > 1:
|
||||
return
|
||||
projects = projects[0]
|
||||
if projects is None:
|
||||
return "root"
|
||||
project: Project = Project.objects(id=projects).only("basename").first()
|
||||
if not project:
|
||||
return
|
||||
|
||||
return project.basename[: conf.get("download.max_project_name_length", 60)]
|
||||
|
||||
call.result.filename = "-".join(
|
||||
filter(None, ("clearml", get_project_name(), f"{request.entity_type}s.csv"))
|
||||
)
|
||||
call.result.content_type = "text/csv"
|
||||
call.result.raw_data = stream_with_context(generate())
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
from apiserver.apimodels.pipelines import StartPipelineResponse, StartPipelineRequest
|
||||
import attr
|
||||
|
||||
from apiserver.apierrors.errors.bad_request import CannotRemoveAllRuns
|
||||
from apiserver.apimodels.pipelines import (
|
||||
StartPipelineResponse,
|
||||
StartPipelineRequest,
|
||||
DeleteRunsRequest,
|
||||
)
|
||||
from apiserver.bll.organization import OrgBLL
|
||||
from apiserver.bll.project import ProjectBLL
|
||||
from apiserver.bll.task import TaskBLL
|
||||
from apiserver.bll.task.task_operations import enqueue_task
|
||||
from apiserver.bll.task.task_operations import enqueue_task, delete_task
|
||||
from apiserver.bll.util import run_batch_operation
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.database.model.task.task import Task, TaskType
|
||||
from apiserver.service_repo import APICall, endpoint
|
||||
|
||||
org_bll = OrgBLL()
|
||||
@@ -31,6 +40,45 @@ def _update_task_name(task: Task):
|
||||
task.update(name=new_name)
|
||||
|
||||
|
||||
@endpoint("pipelines.delete_runs")
|
||||
def delete_runs(call: APICall, company_id: str, request: DeleteRunsRequest):
|
||||
existing_runs = set(
|
||||
Task.objects(project=request.project, type=TaskType.controller).scalar("id")
|
||||
)
|
||||
if not existing_runs.difference(request.ids):
|
||||
raise CannotRemoveAllRuns(project=request.project)
|
||||
|
||||
# make sure that only controller tasks are deleted
|
||||
ids = existing_runs.intersection(request.ids)
|
||||
if not ids:
|
||||
return dict(succeeded=[], failed=[])
|
||||
|
||||
results, failures = run_batch_operation(
|
||||
func=partial(
|
||||
delete_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
move_to_trash=False,
|
||||
force=True,
|
||||
return_file_urls=False,
|
||||
delete_output_models=True,
|
||||
status_message="",
|
||||
status_reason="Pipeline run deleted",
|
||||
delete_external_artifacts=True,
|
||||
),
|
||||
ids=list(ids),
|
||||
)
|
||||
|
||||
succeeded = []
|
||||
if results:
|
||||
for _id, (deleted, task, cleanup_res) in results:
|
||||
succeeded.append(
|
||||
dict(id=_id, deleted=bool(deleted), **attr.asdict(cleanup_res))
|
||||
)
|
||||
|
||||
call.result.data = dict(succeeded=succeeded, failed=failures)
|
||||
|
||||
|
||||
@endpoint(
|
||||
"pipelines.start_pipeline", response_data_model=StartPipelineResponse,
|
||||
)
|
||||
@@ -60,6 +108,7 @@ def start_pipeline(call: APICall, company_id: str, request: StartPipelineRequest
|
||||
queued, res = enqueue_task(
|
||||
task_id=task.id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
queue_id=request.queue,
|
||||
status_message="Starting pipeline",
|
||||
status_reason="",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Sequence
|
||||
from typing import Sequence, Optional, Tuple
|
||||
|
||||
import attr
|
||||
from mongoengine import Q
|
||||
@@ -7,27 +7,33 @@ from apiserver.apierrors import errors
|
||||
from apiserver.apierrors.errors.bad_request import InvalidProjectId
|
||||
from apiserver.apimodels.base import UpdateResponse, MakePublicRequest, IdResponse
|
||||
from apiserver.apimodels.projects import (
|
||||
DeleteRequest,
|
||||
GetParamsRequest,
|
||||
ProjectTagsRequest,
|
||||
ProjectTaskParentsRequest,
|
||||
ProjectHyperparamValuesRequest,
|
||||
ProjectsGetRequest,
|
||||
DeleteRequest,
|
||||
MoveRequest,
|
||||
MergeRequest,
|
||||
ProjectOrNoneRequest,
|
||||
ProjectRequest,
|
||||
ProjectModelMetadataValuesRequest,
|
||||
ProjectChildrenType,
|
||||
GetUniqueMetricsRequest,
|
||||
ProjectUserNamesRequest,
|
||||
EntityTypeEnum,
|
||||
)
|
||||
from apiserver.bll.organization import OrgBLL, Tags
|
||||
from apiserver.bll.project import ProjectBLL, ProjectQueries
|
||||
from apiserver.bll.project.project_bll import pipeline_tag, reports_tag
|
||||
from apiserver.bll.project.project_cleanup import (
|
||||
delete_project,
|
||||
validate_project_delete,
|
||||
)
|
||||
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.task.task import TaskType, Task
|
||||
from apiserver.database.utils import (
|
||||
parse_from_call,
|
||||
get_company_or_none_constraint,
|
||||
@@ -96,30 +102,78 @@ def _adjust_search_parameters(data: dict, shallow_search: bool):
|
||||
data["parent"] = [None]
|
||||
|
||||
|
||||
@endpoint("projects.get_all_ex", request_data_model=ProjectsGetRequest)
|
||||
def _get_project_stats_filter(
|
||||
request: ProjectsGetRequest,
|
||||
) -> Tuple[Optional[dict], bool]:
|
||||
if request.include_stats_filter or not request.children_type:
|
||||
return request.include_stats_filter, request.search_hidden
|
||||
|
||||
if request.children_tags_filter:
|
||||
stats_filter = {"tags": request.children_tags_filter}
|
||||
elif request.children_tags:
|
||||
stats_filter = {"tags": request.children_tags}
|
||||
else:
|
||||
stats_filter = {}
|
||||
|
||||
if request.children_type == ProjectChildrenType.pipeline:
|
||||
return (
|
||||
{
|
||||
**stats_filter,
|
||||
"system_tags": [pipeline_tag],
|
||||
"type": [TaskType.controller],
|
||||
},
|
||||
True,
|
||||
)
|
||||
if request.children_type == ProjectChildrenType.report:
|
||||
return (
|
||||
{**stats_filter, "system_tags": [reports_tag], "type": [TaskType.report]},
|
||||
True,
|
||||
)
|
||||
return stats_filter, request.search_hidden
|
||||
|
||||
|
||||
@endpoint("projects.get_all_ex")
|
||||
def get_all_ex(call: APICall, company_id: str, request: ProjectsGetRequest):
|
||||
data = call.data
|
||||
conform_tag_fields(call, data)
|
||||
allow_public = not request.non_public
|
||||
allow_public = (
|
||||
data["allow_public"]
|
||||
if "allow_public" in data
|
||||
else not data["non_public"]
|
||||
if "non_public" in data
|
||||
else request.allow_public
|
||||
)
|
||||
|
||||
requested_ids = data.get("id")
|
||||
if isinstance(requested_ids, str):
|
||||
requested_ids = [requested_ids]
|
||||
_adjust_search_parameters(
|
||||
data, shallow_search=request.shallow_search,
|
||||
)
|
||||
user_active_project_ids = None
|
||||
if request.active_users:
|
||||
ids, user_active_project_ids = project_bll.get_projects_with_active_user(
|
||||
selected_project_ids = None
|
||||
if request.active_users or request.children_type:
|
||||
ids, selected_project_ids = project_bll.get_projects_with_selected_children(
|
||||
company=company_id,
|
||||
users=request.active_users,
|
||||
project_ids=requested_ids,
|
||||
allow_public=allow_public,
|
||||
children_type=request.children_type,
|
||||
children_tags=request.children_tags,
|
||||
children_tags_filter=request.children_tags_filter,
|
||||
)
|
||||
if not ids:
|
||||
return {"projects": []}
|
||||
data["id"] = ids
|
||||
|
||||
ret_params = {}
|
||||
|
||||
remove_system_tags = False
|
||||
if request.search_hidden:
|
||||
only_fields = data.get("only_fields")
|
||||
if isinstance(only_fields, list) and "system_tags" not in only_fields:
|
||||
only_fields.append("system_tags")
|
||||
remove_system_tags = True
|
||||
|
||||
projects: Sequence[dict] = Project.get_many_with_join(
|
||||
company=company_id,
|
||||
query_dict=data,
|
||||
@@ -130,29 +184,61 @@ def get_all_ex(call: APICall, company_id: str, request: ProjectsGetRequest):
|
||||
if not projects:
|
||||
return {"projects": projects, **ret_params}
|
||||
|
||||
if request.search_hidden:
|
||||
for p in projects:
|
||||
system_tags = (
|
||||
p.pop("system_tags", [])
|
||||
if remove_system_tags
|
||||
else p.get("system_tags", [])
|
||||
)
|
||||
if EntityVisibility.hidden.value in system_tags:
|
||||
p["hidden"] = True
|
||||
|
||||
conform_output_tags(call, projects)
|
||||
project_ids = list({project["id"] for project in projects})
|
||||
|
||||
stats_filter, stats_search_hidden = _get_project_stats_filter(request)
|
||||
if request.check_own_contents:
|
||||
contents = project_bll.calc_own_contents(
|
||||
company=company_id,
|
||||
project_ids=project_ids,
|
||||
filter_=request.include_stats_filter,
|
||||
users=request.active_users,
|
||||
)
|
||||
if request.children_type == ProjectChildrenType.dataset:
|
||||
contents = project_bll.calc_own_datasets(
|
||||
company=company_id,
|
||||
project_ids=project_ids,
|
||||
filter_=stats_filter,
|
||||
users=request.active_users,
|
||||
)
|
||||
else:
|
||||
contents = project_bll.calc_own_contents(
|
||||
company=company_id,
|
||||
project_ids=project_ids,
|
||||
filter_=stats_filter,
|
||||
specific_state=request.stats_for_state,
|
||||
users=request.active_users,
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
project.update(**contents.get(project["id"], {}))
|
||||
|
||||
conform_output_tags(call, projects)
|
||||
if request.include_stats:
|
||||
stats, children = project_bll.get_project_stats(
|
||||
company=company_id,
|
||||
project_ids=project_ids,
|
||||
specific_state=request.stats_for_state,
|
||||
include_children=request.stats_with_children,
|
||||
search_hidden=request.search_hidden,
|
||||
filter_=request.include_stats_filter,
|
||||
users=request.active_users,
|
||||
user_active_project_ids=user_active_project_ids,
|
||||
)
|
||||
if request.children_type == ProjectChildrenType.dataset:
|
||||
stats, children = project_bll.get_project_dataset_stats(
|
||||
company=company_id,
|
||||
project_ids=project_ids,
|
||||
include_children=request.stats_with_children,
|
||||
filter_=stats_filter,
|
||||
users=request.active_users,
|
||||
selected_project_ids=selected_project_ids,
|
||||
)
|
||||
else:
|
||||
stats, children = project_bll.get_project_stats(
|
||||
company=company_id,
|
||||
project_ids=project_ids,
|
||||
specific_state=request.stats_for_state,
|
||||
include_children=request.stats_with_children,
|
||||
search_hidden=stats_search_hidden,
|
||||
filter_=stats_filter,
|
||||
users=request.active_users,
|
||||
selected_project_ids=selected_project_ids,
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
project["stats"] = stats[project["id"]]
|
||||
@@ -277,28 +363,30 @@ def delete(call: APICall, company_id: str, request: DeleteRequest):
|
||||
project_id=request.project,
|
||||
force=request.force,
|
||||
delete_contents=request.delete_contents,
|
||||
delete_external_artifacts=request.delete_external_artifacts,
|
||||
)
|
||||
_reset_cached_tags(company_id, projects=list(affected_projects))
|
||||
call.result.data = {**attr.asdict(res)}
|
||||
|
||||
|
||||
@endpoint(
|
||||
"projects.get_unique_metric_variants", request_data_model=ProjectOrNoneRequest
|
||||
"projects.get_unique_metric_variants", request_data_model=GetUniqueMetricsRequest
|
||||
)
|
||||
def get_unique_metric_variants(
|
||||
call: APICall, company_id: str, request: ProjectOrNoneRequest
|
||||
call: APICall, company_id: str, request: GetUniqueMetricsRequest
|
||||
):
|
||||
|
||||
metrics = project_queries.get_unique_metric_variants(
|
||||
company_id,
|
||||
[request.project] if request.project else None,
|
||||
include_subprojects=request.include_subprojects,
|
||||
model_metrics=request.model_metrics,
|
||||
)
|
||||
|
||||
call.result.data = {"metrics": metrics}
|
||||
|
||||
|
||||
@endpoint("projects.get_model_metadata_keys",)
|
||||
@endpoint("projects.get_model_metadata_keys")
|
||||
def get_model_metadata_keys(call: APICall, company_id: str, request: GetParamsRequest):
|
||||
total, remaining, keys = project_queries.get_model_metadata_keys(
|
||||
company_id,
|
||||
@@ -325,6 +413,8 @@ def get_model_metadata_values(
|
||||
key=request.key,
|
||||
include_subprojects=request.include_subprojects,
|
||||
allow_public=request.allow_public,
|
||||
page=request.page,
|
||||
page_size=request.page_size,
|
||||
)
|
||||
call.result.data = {
|
||||
"total": total,
|
||||
@@ -369,6 +459,9 @@ def get_hyperparam_values(
|
||||
name=request.name,
|
||||
include_subprojects=request.include_subprojects,
|
||||
allow_public=request.allow_public,
|
||||
pattern=request.pattern,
|
||||
page=request.page,
|
||||
page_size=request.page_size,
|
||||
)
|
||||
call.result.data = {
|
||||
"total": total,
|
||||
@@ -420,7 +513,11 @@ def get_tags(call: APICall, company, request: ProjectTagsRequest):
|
||||
)
|
||||
def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
call.result.data = Project.set_public(
|
||||
company_id, ids=request.ids, invalid_cls=InvalidProjectId, enabled=True
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
ids=request.ids,
|
||||
invalid_cls=InvalidProjectId,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -429,7 +526,11 @@ def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
)
|
||||
def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
call.result.data = Project.set_public(
|
||||
company_id, ids=request.ids, invalid_cls=InvalidProjectId, enabled=False
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
ids=request.ids,
|
||||
invalid_cls=InvalidProjectId,
|
||||
enabled=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -442,10 +543,23 @@ def get_task_parents(
|
||||
call: APICall, company_id: str, request: ProjectTaskParentsRequest
|
||||
):
|
||||
call.result.data = {
|
||||
"parents": project_bll.get_task_parents(
|
||||
"parents": ProjectBLL.get_task_parents(
|
||||
company_id,
|
||||
projects=request.projects,
|
||||
include_subprojects=request.include_subprojects,
|
||||
state=request.tasks_state,
|
||||
name=request.task_name,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@endpoint("projects.get_user_names")
|
||||
def get_user_names(call: APICall, company_id: str, request: ProjectUserNamesRequest):
|
||||
call.result.data = {
|
||||
"users": ProjectBLL.get_entity_users(
|
||||
company_id,
|
||||
entity_cls=Model if request.entity == EntityTypeEnum.model else Task,
|
||||
projects=request.projects,
|
||||
include_subprojects=request.include_subprojects,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Union, Sequence
|
||||
|
||||
from mongoengine import Q
|
||||
|
||||
from apiserver.apimodels.base import UpdateResponse
|
||||
@@ -21,6 +23,7 @@ from apiserver.apimodels.queues import (
|
||||
)
|
||||
from apiserver.bll.model import Metadata
|
||||
from apiserver.bll.queue import QueueBLL
|
||||
from apiserver.bll.queue.queue_bll import MOVE_FIRST, MOVE_LAST
|
||||
from apiserver.bll.workers import WorkerBLL
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model.task.task import Task
|
||||
@@ -38,14 +41,18 @@ worker_bll = WorkerBLL()
|
||||
queue_bll = QueueBLL(worker_bll)
|
||||
|
||||
|
||||
def conform_queue_data(call: APICall, queue_data: Union[Sequence[dict], dict]):
|
||||
conform_output_tags(call, queue_data)
|
||||
unescape_metadata(call, queue_data)
|
||||
|
||||
|
||||
@endpoint("queues.get_by_id", min_version="2.4", request_data_model=GetByIdRequest)
|
||||
def get_by_id(call: APICall, company_id, request: GetByIdRequest):
|
||||
queue = queue_bll.get_by_id(
|
||||
company_id, request.queue, max_task_entries=request.max_task_entries
|
||||
)
|
||||
queue_dict = queue.to_proper_dict()
|
||||
conform_output_tags(call, queue_dict)
|
||||
unescape_metadata(call, queue_dict)
|
||||
conform_queue_data(call, queue_dict)
|
||||
call.result.data = {"queue": queue_dict}
|
||||
|
||||
|
||||
@@ -76,16 +83,15 @@ def get_all_ex(call: APICall, company: str, request: GetAllRequest):
|
||||
conform_tag_fields(call, call.data)
|
||||
ret_params = {}
|
||||
|
||||
Metadata.escape_query_parameters(call)
|
||||
call_data = Metadata.escape_query_parameters(call.data)
|
||||
queues = queue_bll.get_queue_infos(
|
||||
company_id=company,
|
||||
query_dict=call.data,
|
||||
query=_hidden_query(call.data),
|
||||
query_dict=call_data,
|
||||
query=_hidden_query(call_data),
|
||||
max_task_entries=request.max_task_entries,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
conform_output_tags(call, queues)
|
||||
unescape_metadata(call, queues)
|
||||
conform_queue_data(call, queues)
|
||||
call.result.data = {"queues": queues, **ret_params}
|
||||
|
||||
|
||||
@@ -93,16 +99,15 @@ def get_all_ex(call: APICall, company: str, request: GetAllRequest):
|
||||
def get_all(call: APICall, company: str, request: GetAllRequest):
|
||||
conform_tag_fields(call, call.data)
|
||||
ret_params = {}
|
||||
Metadata.escape_query_parameters(call)
|
||||
call_data = Metadata.escape_query_parameters(call.data)
|
||||
queues = queue_bll.get_all(
|
||||
company_id=company,
|
||||
query_dict=call.data,
|
||||
query=_hidden_query(call.data),
|
||||
query_dict=call_data,
|
||||
query=_hidden_query(call_data),
|
||||
max_task_entries=request.max_task_entries,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
conform_output_tags(call, queues)
|
||||
unescape_metadata(call, queues)
|
||||
conform_queue_data(call, queues)
|
||||
call.result.data = {"queues": queues, **ret_params}
|
||||
|
||||
|
||||
@@ -134,15 +139,17 @@ def update(call: APICall, company_id, req_model: UpdateRequest):
|
||||
updated, fields = queue_bll.update(
|
||||
company_id=company_id, queue_id=req_model.queue, **data
|
||||
)
|
||||
conform_output_tags(call, fields)
|
||||
unescape_metadata(call, fields)
|
||||
conform_queue_data(call, fields)
|
||||
call.result.data_model = UpdateResponse(updated=updated, fields=fields)
|
||||
|
||||
|
||||
@endpoint("queues.delete", min_version="2.4", request_data_model=DeleteRequest)
|
||||
def delete(call: APICall, company_id, req_model: DeleteRequest):
|
||||
queue_bll.delete(
|
||||
company_id=company_id, queue_id=req_model.queue, force=req_model.force
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
queue_id=req_model.queue,
|
||||
force=req_model.force,
|
||||
)
|
||||
call.result.data = {"deleted": 1}
|
||||
|
||||
@@ -164,7 +171,7 @@ def get_next_task(call: APICall, company_id, request: GetNextTaskRequest):
|
||||
if entry:
|
||||
data = {"entry": entry.to_proper_dict()}
|
||||
if request.get_task_info:
|
||||
task = Task.objects(id=entry.task).first()
|
||||
task = Task.objects(id=entry.task).only("company", "user").first()
|
||||
if task:
|
||||
data["task_info"] = {"company": task.company, "user": task.user}
|
||||
|
||||
@@ -192,7 +199,7 @@ def move_task_forward(call: APICall, company_id, req_model: MoveTaskRequest):
|
||||
company_id=company_id,
|
||||
queue_id=req_model.queue,
|
||||
task_id=req_model.task,
|
||||
pos_func=lambda p: max(0, p - req_model.count),
|
||||
move_count=-req_model.count,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -209,7 +216,7 @@ def move_task_backward(call: APICall, company_id, req_model: MoveTaskRequest):
|
||||
company_id=company_id,
|
||||
queue_id=req_model.queue,
|
||||
task_id=req_model.task,
|
||||
pos_func=lambda p: max(0, p + req_model.count),
|
||||
move_count=req_model.count,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -226,7 +233,7 @@ def move_task_to_front(call: APICall, company_id, req_model: TaskRequest):
|
||||
company_id=company_id,
|
||||
queue_id=req_model.queue,
|
||||
task_id=req_model.task,
|
||||
pos_func=lambda p: 0,
|
||||
move_count=MOVE_FIRST,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -243,7 +250,7 @@ def move_task_to_back(call: APICall, company_id, req_model: TaskRequest):
|
||||
company_id=company_id,
|
||||
queue_id=req_model.queue,
|
||||
task_id=req_model.task,
|
||||
pos_func=lambda p: -1,
|
||||
move_count=MOVE_LAST,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
420
apiserver/services/reports.py
Normal file
420
apiserver/services/reports.py
Normal file
@@ -0,0 +1,420 @@
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from typing import Sequence
|
||||
|
||||
from mongoengine import Q
|
||||
|
||||
from apiserver.apimodels.reports import (
|
||||
CreateReportRequest,
|
||||
UpdateReportRequest,
|
||||
PublishReportRequest,
|
||||
ArchiveReportRequest,
|
||||
DeleteReportRequest,
|
||||
MoveReportRequest,
|
||||
GetTasksDataRequest,
|
||||
EventsRequest,
|
||||
GetAllRequest,
|
||||
)
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels.base import UpdateResponse
|
||||
from apiserver.bll.project.project_bll import reports_project_name, reports_tag
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.services.models import conform_model_data
|
||||
from apiserver.services.utils import process_include_subprojects, sort_tags_response
|
||||
from apiserver.bll.organization import OrgBLL
|
||||
from apiserver.bll.project import ProjectBLL
|
||||
from apiserver.bll.task import TaskBLL, ChangeStatusRequest
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.task.task import Task, TaskType, TaskStatus
|
||||
from apiserver.service_repo import APICall, endpoint
|
||||
from apiserver.services.events import (
|
||||
_get_task_or_model_index_companies,
|
||||
event_bll,
|
||||
_get_metrics_response,
|
||||
_get_metric_variants_from_request,
|
||||
_get_multitask_plots,
|
||||
_get_single_value_metrics_response,
|
||||
)
|
||||
from apiserver.services.tasks import (
|
||||
escape_execution_parameters,
|
||||
_hidden_query,
|
||||
conform_task_data,
|
||||
)
|
||||
|
||||
org_bll = OrgBLL()
|
||||
project_bll = ProjectBLL()
|
||||
task_bll = TaskBLL()
|
||||
|
||||
|
||||
update_fields = {
|
||||
"name",
|
||||
"tags",
|
||||
"comment",
|
||||
"report",
|
||||
"report_assets",
|
||||
}
|
||||
|
||||
|
||||
def _assert_report(company_id, task_id, only_fields=None, requires_write_access=True):
|
||||
if only_fields and "type" not in only_fields:
|
||||
only_fields += ("type",)
|
||||
|
||||
task = TaskBLL.get_task_with_access(
|
||||
task_id=task_id,
|
||||
company_id=company_id,
|
||||
only=only_fields,
|
||||
requires_write_access=requires_write_access,
|
||||
)
|
||||
if task.type != TaskType.report:
|
||||
raise errors.bad_request.OperationSupportedOnReportsOnly(id=task_id)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
@endpoint("reports.update", response_data_model=UpdateResponse)
|
||||
def update_report(call: APICall, company_id: str, request: UpdateReportRequest):
|
||||
task = _assert_report(
|
||||
task_id=request.task,
|
||||
company_id=company_id,
|
||||
only_fields=("status",),
|
||||
)
|
||||
|
||||
partial_update_dict = {
|
||||
field: value for field, value in call.data.items() if field in update_fields
|
||||
}
|
||||
if not partial_update_dict:
|
||||
return UpdateResponse(updated=0)
|
||||
|
||||
allowed_for_published = set(partial_update_dict.keys()).issubset(
|
||||
{"tags", "name", "comment"}
|
||||
)
|
||||
if task.status != TaskStatus.created and not allowed_for_published:
|
||||
raise errors.bad_request.InvalidTaskStatus(
|
||||
expected=TaskStatus.created, status=task.status
|
||||
)
|
||||
|
||||
now = datetime.utcnow()
|
||||
more_updates = {"last_change": now, "last_changed_by": call.identity.user}
|
||||
if not allowed_for_published:
|
||||
more_updates["last_update"] = now
|
||||
|
||||
updated = task.update(upsert=False, **partial_update_dict, **more_updates)
|
||||
if not updated:
|
||||
return UpdateResponse(updated=0)
|
||||
|
||||
updated_tags = partial_update_dict.get("tags")
|
||||
if updated_tags:
|
||||
partial_update_dict["tags"] = sorted(updated_tags)
|
||||
updated_report = partial_update_dict.get("report")
|
||||
if updated_report:
|
||||
partial_update_dict["report"] = textwrap.shorten(updated_report, width=100)
|
||||
|
||||
return UpdateResponse(updated=updated, fields=partial_update_dict)
|
||||
|
||||
|
||||
def _ensure_reports_project(company: str, user: str, name: str):
|
||||
name = name.strip("/")
|
||||
_, _, basename = name.rpartition("/")
|
||||
if basename != reports_project_name:
|
||||
name = f"{name}/{reports_project_name}"
|
||||
|
||||
return project_bll.find_or_create(
|
||||
user=user,
|
||||
company=company,
|
||||
project_name=name,
|
||||
description="Reports project",
|
||||
system_tags=[reports_tag, EntityVisibility.hidden.value],
|
||||
)
|
||||
|
||||
|
||||
@endpoint("reports.create")
|
||||
def create_report(call: APICall, company_id: str, request: CreateReportRequest):
|
||||
user_id = call.identity.user
|
||||
project_id = request.project
|
||||
if request.project:
|
||||
project = Project.get_for_writing(
|
||||
company=company_id, id=project_id, _only=("name",)
|
||||
)
|
||||
project_name = project.name
|
||||
else:
|
||||
project_name = ""
|
||||
|
||||
project_id = _ensure_reports_project(
|
||||
company=company_id, user=user_id, name=project_name
|
||||
)
|
||||
task = task_bll.create(
|
||||
company=company_id,
|
||||
user=user_id,
|
||||
fields=dict(
|
||||
project=project_id,
|
||||
name=request.name,
|
||||
tags=request.tags,
|
||||
comment=request.comment,
|
||||
type=TaskType.report,
|
||||
system_tags=[reports_tag, EntityVisibility.hidden.value],
|
||||
),
|
||||
)
|
||||
task.save()
|
||||
|
||||
call.result.data = {"id": task.id, "project_id": project_id}
|
||||
|
||||
|
||||
def _delete_reports_project_if_empty(project_id):
|
||||
project = Project.objects(id=project_id).only("basename").first()
|
||||
if (
|
||||
project
|
||||
and project.basename == reports_project_name
|
||||
and Task.objects(project=project_id).count() == 0
|
||||
):
|
||||
project.delete()
|
||||
|
||||
|
||||
@endpoint("reports.get_all_ex")
|
||||
def get_all_ex(call: APICall, company_id, request: GetAllRequest):
|
||||
call_data = call.data
|
||||
call_data["type"] = TaskType.report
|
||||
process_include_subprojects(call_data)
|
||||
# bring projects one level down in case not the .reports project was passed
|
||||
if "project" in call_data:
|
||||
project_ids = call_data["project"]
|
||||
if not isinstance(project_ids, list):
|
||||
project_ids = [project_ids]
|
||||
|
||||
query = Q(parent__in=project_ids) | Q(id__in=project_ids)
|
||||
project_ids = Project.objects(query & Q(basename=reports_project_name)).scalar(
|
||||
"id"
|
||||
)
|
||||
if not project_ids:
|
||||
return {"tasks": []}
|
||||
call_data["project"] = list(project_ids)
|
||||
|
||||
ret_params = {}
|
||||
tasks = Task.get_many_with_join(
|
||||
company=company_id,
|
||||
query_dict=call_data,
|
||||
allow_public=request.allow_public,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
conform_task_data(call, tasks)
|
||||
|
||||
call.result.data = {"tasks": tasks, **ret_params}
|
||||
|
||||
|
||||
def _get_task_metrics_from_request(
|
||||
task_ids: Sequence[str], request: EventsRequest
|
||||
) -> dict:
|
||||
task_metrics = {}
|
||||
for task in task_ids:
|
||||
task_dict = {}
|
||||
for mv in request.metrics:
|
||||
task_dict[mv.metric] = mv.variants
|
||||
task_metrics[task] = task_dict
|
||||
|
||||
return task_metrics
|
||||
|
||||
|
||||
@endpoint("reports.get_task_data")
|
||||
def get_task_data(call: APICall, company_id, request: GetTasksDataRequest):
|
||||
if request.model_events:
|
||||
entity_cls = Model
|
||||
conform_data = conform_model_data
|
||||
else:
|
||||
entity_cls = Task
|
||||
conform_data = conform_task_data
|
||||
|
||||
call_data = escape_execution_parameters(call.data)
|
||||
process_include_subprojects(call_data)
|
||||
|
||||
ret_params = {}
|
||||
tasks = entity_cls.get_many_with_join(
|
||||
company=company_id,
|
||||
query_dict=call_data,
|
||||
query=_hidden_query(call_data),
|
||||
allow_public=request.allow_public,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
conform_data(call, tasks)
|
||||
res = {"tasks": tasks, **ret_params}
|
||||
if not (
|
||||
request.debug_images
|
||||
or request.plots
|
||||
or request.scalar_metrics_iter_histogram
|
||||
or request.single_value_metrics
|
||||
):
|
||||
return res
|
||||
|
||||
task_ids = [task["id"] for task in tasks]
|
||||
companies = _get_task_or_model_index_companies(
|
||||
company_id, task_ids=task_ids, model_events=request.model_events
|
||||
)
|
||||
if request.debug_images:
|
||||
result = event_bll.debug_images_iterator.get_task_events(
|
||||
companies={
|
||||
t.id: t.company for t in chain.from_iterable(companies.values())
|
||||
},
|
||||
task_metrics=_get_task_metrics_from_request(task_ids, request.debug_images),
|
||||
iter_count=request.debug_images.iters,
|
||||
)
|
||||
res["debug_images"] = [
|
||||
r.to_struct() for r in _get_metrics_response(result.metric_events)
|
||||
]
|
||||
|
||||
if request.plots:
|
||||
res["plots"] = _get_multitask_plots(
|
||||
companies=companies,
|
||||
last_iters=request.plots.iters,
|
||||
metrics=_get_metric_variants_from_request(request.plots.metrics),
|
||||
last_iters_per_task_metric=request.plots.last_iters_per_task_metric,
|
||||
)[0]
|
||||
|
||||
if request.scalar_metrics_iter_histogram:
|
||||
res[
|
||||
"scalar_metrics_iter_histogram"
|
||||
] = event_bll.metrics.compare_scalar_metrics_average_per_iter(
|
||||
companies=companies,
|
||||
samples=request.scalar_metrics_iter_histogram.samples,
|
||||
key=request.scalar_metrics_iter_histogram.key,
|
||||
metric_variants=_get_metric_variants_from_request(
|
||||
request.scalar_metrics_iter_histogram.metrics
|
||||
),
|
||||
)
|
||||
|
||||
if request.single_value_metrics:
|
||||
res["single_value_metrics"] = _get_single_value_metrics_response(
|
||||
companies=companies,
|
||||
value_metrics=event_bll.metrics.get_task_single_value_metrics(
|
||||
companies=companies
|
||||
),
|
||||
)
|
||||
|
||||
call.result.data = res
|
||||
|
||||
|
||||
@endpoint("reports.move")
|
||||
def move(call: APICall, company_id: str, request: MoveReportRequest):
|
||||
if not ("project" in call.data or request.project_name):
|
||||
raise errors.bad_request.MissingRequiredFields(
|
||||
"project or project_name is required"
|
||||
)
|
||||
|
||||
task = _assert_report(
|
||||
company_id=company_id,
|
||||
task_id=request.task,
|
||||
only_fields=("project",),
|
||||
)
|
||||
user_id = call.identity.user
|
||||
project_name = request.project_name
|
||||
if not project_name:
|
||||
if request.project:
|
||||
project = Project.get_for_writing(
|
||||
company=company_id, id=request.project, _only=("name",)
|
||||
)
|
||||
project_name = project.name
|
||||
else:
|
||||
project_name = ""
|
||||
|
||||
project_id = _ensure_reports_project(
|
||||
company=company_id, user=user_id, name=project_name
|
||||
)
|
||||
|
||||
project_bll.move_under_project(
|
||||
entity_cls=Task,
|
||||
user=call.identity.user,
|
||||
company=company_id,
|
||||
ids=[request.task],
|
||||
project=project_id,
|
||||
)
|
||||
|
||||
_delete_reports_project_if_empty(task.project)
|
||||
|
||||
return {"project_id": project_id}
|
||||
|
||||
|
||||
@endpoint(
|
||||
"reports.publish",
|
||||
response_data_model=UpdateResponse,
|
||||
)
|
||||
def publish(call: APICall, company_id, request: PublishReportRequest):
|
||||
task = _assert_report(company_id=company_id, task_id=request.task)
|
||||
updates = ChangeStatusRequest(
|
||||
task=task,
|
||||
new_status=TaskStatus.published,
|
||||
force=True,
|
||||
status_reason="",
|
||||
status_message=request.message,
|
||||
user_id=call.identity.user,
|
||||
).execute(published=datetime.utcnow())
|
||||
|
||||
call.result.data_model = UpdateResponse(**updates)
|
||||
|
||||
|
||||
@endpoint("reports.archive")
|
||||
def archive(call: APICall, company_id, request: ArchiveReportRequest):
|
||||
task = _assert_report(company_id=company_id, task_id=request.task)
|
||||
archived = task.update(
|
||||
status_message=request.message,
|
||||
status_reason="",
|
||||
add_to_set__system_tags=EntityVisibility.archived.value,
|
||||
last_change=datetime.utcnow(),
|
||||
last_changed_by=call.identity.user,
|
||||
)
|
||||
|
||||
return {"archived": archived}
|
||||
|
||||
|
||||
@endpoint("reports.unarchive")
|
||||
def unarchive(call: APICall, company_id, request: ArchiveReportRequest):
|
||||
task = _assert_report(company_id=company_id, task_id=request.task)
|
||||
unarchived = task.update(
|
||||
status_message=request.message,
|
||||
status_reason="",
|
||||
pull__system_tags=EntityVisibility.archived.value,
|
||||
last_change=datetime.utcnow(),
|
||||
last_changed_by=call.identity.user,
|
||||
)
|
||||
return {"unarchived": unarchived}
|
||||
|
||||
|
||||
# @endpoint("reports.share")
|
||||
# def share(call: APICall, company_id, request: ShareReportRequest):
|
||||
# _assert_report(
|
||||
# company_id=company_id, user_id=call.identity.user, task_id=request.task
|
||||
# )
|
||||
# call.result.data = {
|
||||
# "changed": task_bll.share_task(
|
||||
# company_id=company_id, task_ids=[request.task], share=request.share
|
||||
# )
|
||||
# }
|
||||
|
||||
|
||||
@endpoint("reports.delete")
|
||||
def delete(call: APICall, company_id, request: DeleteReportRequest):
|
||||
task = _assert_report(
|
||||
company_id=company_id,
|
||||
task_id=request.task,
|
||||
only_fields=("project",),
|
||||
)
|
||||
if (
|
||||
task.status != TaskStatus.created
|
||||
and EntityVisibility.archived.value not in task.system_tags
|
||||
and not request.force
|
||||
):
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
"due to status, use force=True",
|
||||
task=task.id,
|
||||
expected=TaskStatus.created,
|
||||
current=task.status,
|
||||
)
|
||||
|
||||
task.delete()
|
||||
_delete_reports_project_if_empty(task.project)
|
||||
|
||||
call.result.data = {"deleted": 1}
|
||||
|
||||
|
||||
@endpoint("reports.get_tags")
|
||||
def get_tags(call: APICall, company_id: str, _):
|
||||
tags = Task.objects(company=company_id, type=TaskType.report).distinct(field="tags")
|
||||
call.result.data = sort_tags_response({"tags": tags})
|
||||
@@ -64,6 +64,10 @@ from apiserver.apimodels.tasks import (
|
||||
ResetBatchItem,
|
||||
CompletedRequest,
|
||||
CompletedResponse,
|
||||
GetAllReq,
|
||||
DequeueRequest,
|
||||
DequeueManyRequest,
|
||||
UpdateTagsRequest,
|
||||
)
|
||||
from apiserver.bll.event import EventBLL
|
||||
from apiserver.bll.model import ModelBLL
|
||||
@@ -73,7 +77,6 @@ from apiserver.bll.queue import QueueBLL
|
||||
from apiserver.bll.task import (
|
||||
TaskBLL,
|
||||
ChangeStatusRequest,
|
||||
update_project_time,
|
||||
)
|
||||
from apiserver.bll.task.artifacts import (
|
||||
artifacts_prepare_for_save,
|
||||
@@ -98,7 +101,7 @@ from apiserver.bll.task.task_operations import (
|
||||
move_tasks_to_trash,
|
||||
)
|
||||
from apiserver.bll.task.utils import update_task, get_task_for_update, deleted_prefix
|
||||
from apiserver.bll.util import SetFieldsResolver, run_batch_operation
|
||||
from apiserver.bll.util import run_batch_operation, update_project_time
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.task.output import Output
|
||||
@@ -109,7 +112,11 @@ from apiserver.database.model.task.task import (
|
||||
ModelItem,
|
||||
TaskModelTypes,
|
||||
)
|
||||
from apiserver.database.utils import get_fields_attr, parse_from_call, get_options
|
||||
from apiserver.database.utils import (
|
||||
get_fields_attr,
|
||||
parse_from_call,
|
||||
get_options,
|
||||
)
|
||||
from apiserver.service_repo import APICall, endpoint
|
||||
from apiserver.services.utils import (
|
||||
conform_tag_fields,
|
||||
@@ -136,32 +143,15 @@ project_bll = ProjectBLL()
|
||||
|
||||
|
||||
def set_task_status_from_call(
|
||||
request: UpdateRequest, company_id, new_status=None, **set_fields
|
||||
request: UpdateRequest, company_id: str, user_id: str, new_status=None, **set_fields
|
||||
) -> dict:
|
||||
fields_resolver = SetFieldsResolver(set_fields)
|
||||
task = TaskBLL.get_task_with_access(
|
||||
request.task,
|
||||
company_id=company_id,
|
||||
only=tuple(
|
||||
{"status", "project", "started", "duration"} | fields_resolver.get_names()
|
||||
),
|
||||
only=("id", "status", "project"),
|
||||
requires_write_access=True,
|
||||
)
|
||||
|
||||
if "duration" not in fields_resolver.get_names():
|
||||
if new_status == Task.started:
|
||||
fields_resolver.add_fields(min__duration=max(0, task.duration or 0))
|
||||
elif new_status in (
|
||||
TaskStatus.completed,
|
||||
TaskStatus.failed,
|
||||
TaskStatus.stopped,
|
||||
):
|
||||
fields_resolver.add_fields(
|
||||
duration=int((task.started - datetime.utcnow()).total_seconds())
|
||||
if task.started
|
||||
else 0
|
||||
)
|
||||
|
||||
status_reason = request.status_reason
|
||||
status_message = request.status_message
|
||||
force = request.force
|
||||
@@ -171,7 +161,8 @@ def set_task_status_from_call(
|
||||
status_reason=status_reason,
|
||||
status_message=status_message,
|
||||
force=force,
|
||||
).execute(**fields_resolver.get_fields(task))
|
||||
user_id=user_id,
|
||||
).execute(**set_fields)
|
||||
|
||||
|
||||
@endpoint("tasks.get_by_id", request_data_model=TaskRequest)
|
||||
@@ -180,17 +171,17 @@ def get_by_id(call: APICall, company_id, req_model: TaskRequest):
|
||||
req_model.task, company_id=company_id, allow_public=True
|
||||
)
|
||||
task_dict = task.to_proper_dict()
|
||||
unprepare_from_saved(call, task_dict)
|
||||
conform_task_data(call, task_dict)
|
||||
call.result.data = {"task": task_dict}
|
||||
|
||||
|
||||
def escape_execution_parameters(call: APICall) -> dict:
|
||||
if not call.data:
|
||||
return call.data
|
||||
def escape_execution_parameters(call_data: dict) -> dict:
|
||||
if not call_data:
|
||||
return call_data
|
||||
|
||||
keys = list(call.data)
|
||||
keys = list(call_data)
|
||||
call_data = {
|
||||
safe_key: call.data[key] for key, safe_key in zip(keys, escape_paths(keys))
|
||||
safe_key: call_data[key] for key, safe_key in zip(keys, escape_paths(keys))
|
||||
}
|
||||
|
||||
projection = Task.get_projection(call_data)
|
||||
@@ -214,44 +205,40 @@ def _hidden_query(data: dict) -> Q:
|
||||
return Q(system_tags__ne=EntityVisibility.hidden.value)
|
||||
|
||||
|
||||
@endpoint("tasks.get_all_ex", required_fields=[])
|
||||
def get_all_ex(call: APICall, company_id, _):
|
||||
@endpoint("tasks.get_all_ex")
|
||||
def get_all_ex(call: APICall, company_id, request: GetAllReq):
|
||||
conform_tag_fields(call, call.data)
|
||||
|
||||
call_data = escape_execution_parameters(call)
|
||||
|
||||
call_data = escape_execution_parameters(call.data)
|
||||
process_include_subprojects(call_data)
|
||||
ret_params = {}
|
||||
tasks = Task.get_many_with_join(
|
||||
company=company_id,
|
||||
query_dict=call_data,
|
||||
query=_hidden_query(call_data),
|
||||
allow_public=True,
|
||||
allow_public=request.allow_public,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
unprepare_from_saved(call, tasks)
|
||||
conform_task_data(call, tasks)
|
||||
call.result.data = {"tasks": tasks, **ret_params}
|
||||
|
||||
|
||||
@endpoint("tasks.get_by_id_ex", required_fields=["id"])
|
||||
def get_by_id_ex(call: APICall, company_id, _):
|
||||
conform_tag_fields(call, call.data)
|
||||
|
||||
call_data = escape_execution_parameters(call)
|
||||
|
||||
call_data = escape_execution_parameters(call.data)
|
||||
tasks = Task.get_many_with_join(
|
||||
company=company_id, query_dict=call_data, allow_public=True,
|
||||
)
|
||||
|
||||
unprepare_from_saved(call, tasks)
|
||||
conform_task_data(call, tasks)
|
||||
call.result.data = {"tasks": tasks}
|
||||
|
||||
|
||||
@endpoint("tasks.get_all", required_fields=[])
|
||||
def get_all(call: APICall, company_id, _):
|
||||
conform_tag_fields(call, call.data)
|
||||
|
||||
call_data = escape_execution_parameters(call)
|
||||
call_data = escape_execution_parameters(call.data)
|
||||
process_include_subprojects(call_data)
|
||||
|
||||
ret_params = {}
|
||||
tasks = Task.get_many(
|
||||
@@ -262,7 +249,7 @@ def get_all(call: APICall, company_id, _):
|
||||
allow_public=True,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
unprepare_from_saved(call, tasks)
|
||||
conform_task_data(call, tasks)
|
||||
call.result.data = {"tasks": tasks, **ret_params}
|
||||
|
||||
|
||||
@@ -291,6 +278,7 @@ def stop(call: APICall, company_id, req_model: UpdateRequest):
|
||||
**stop_task(
|
||||
task_id=req_model.task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
user_name=call.identity.user_name,
|
||||
status_reason=req_model.status_reason,
|
||||
force=req_model.force,
|
||||
@@ -308,6 +296,7 @@ def stop_many(call: APICall, company_id, request: StopManyRequest):
|
||||
func=partial(
|
||||
stop_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
user_name=call.identity.user_name,
|
||||
status_reason=request.status_reason,
|
||||
force=request.force,
|
||||
@@ -329,7 +318,8 @@ def stopped(call: APICall, company_id, req_model: UpdateRequest):
|
||||
call.result.data_model = UpdateResponse(
|
||||
**set_task_status_from_call(
|
||||
req_model,
|
||||
company_id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
new_status=TaskStatus.stopped,
|
||||
completed=datetime.utcnow(),
|
||||
)
|
||||
@@ -345,7 +335,8 @@ def started(call: APICall, company_id, req_model: UpdateRequest):
|
||||
res = StartedResponse(
|
||||
**set_task_status_from_call(
|
||||
req_model,
|
||||
company_id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
new_status=TaskStatus.in_progress,
|
||||
min__started=datetime.utcnow(), # don't override a previous, smaller "started" field value
|
||||
)
|
||||
@@ -359,7 +350,12 @@ def started(call: APICall, company_id, req_model: UpdateRequest):
|
||||
)
|
||||
def failed(call: APICall, company_id, req_model: UpdateRequest):
|
||||
call.result.data_model = UpdateResponse(
|
||||
**set_task_status_from_call(req_model, company_id, new_status=TaskStatus.failed)
|
||||
**set_task_status_from_call(
|
||||
req_model,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
new_status=TaskStatus.failed,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -368,7 +364,12 @@ def failed(call: APICall, company_id, req_model: UpdateRequest):
|
||||
)
|
||||
def close(call: APICall, company_id, req_model: UpdateRequest):
|
||||
call.result.data_model = UpdateResponse(
|
||||
**set_task_status_from_call(req_model, company_id, new_status=TaskStatus.closed)
|
||||
**set_task_status_from_call(
|
||||
req_model,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
new_status=TaskStatus.closed,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -415,7 +416,7 @@ def prepare_for_save(call: APICall, fields: dict, previous_task: Task = None):
|
||||
return fields
|
||||
|
||||
|
||||
def unprepare_from_saved(call: APICall, tasks_data: Union[Sequence[dict], dict]):
|
||||
def conform_task_data(call: APICall, tasks_data: Union[Sequence[dict], dict]):
|
||||
if isinstance(tasks_data, dict):
|
||||
tasks_data = [tasks_data]
|
||||
|
||||
@@ -475,7 +476,9 @@ def _validate_and_get_task_from_call(call: APICall, **kwargs) -> Tuple[Task, dic
|
||||
field_does_not_exist_cls=errors.bad_request.ValidationError
|
||||
):
|
||||
fields = prepare_create_fields(call, **kwargs)
|
||||
task = task_bll.create(call, fields)
|
||||
task = task_bll.create(
|
||||
company=call.identity.company, user=call.identity.user, fields=fields
|
||||
)
|
||||
|
||||
task_bll.validate(task)
|
||||
|
||||
@@ -494,7 +497,7 @@ def _update_cached_tags(company: str, project: str, fields: dict):
|
||||
org_bll.update_tags(
|
||||
company,
|
||||
Tags.Task,
|
||||
project=project,
|
||||
projects=[project],
|
||||
tags=fields.get("tags"),
|
||||
system_tags=fields.get("system_tags"),
|
||||
)
|
||||
@@ -578,7 +581,9 @@ def update(call: APICall, company_id, req_model: UpdateRequest):
|
||||
company_id=company_id,
|
||||
id=task_id,
|
||||
partial_update_dict=partial_update_dict,
|
||||
injected_update=dict(last_change=datetime.utcnow()),
|
||||
injected_update=dict(
|
||||
last_change=datetime.utcnow(), last_changed_by=call.identity.user,
|
||||
),
|
||||
)
|
||||
if updated_count:
|
||||
new_project = updated_fields.get("project", task.project)
|
||||
@@ -589,7 +594,7 @@ def update(call: APICall, company_id, req_model: UpdateRequest):
|
||||
company_id, project=task.project, fields=updated_fields
|
||||
)
|
||||
update_project_time(updated_fields.get("project"))
|
||||
unprepare_from_saved(call, updated_fields)
|
||||
conform_task_data(call, updated_fields)
|
||||
return UpdateResponse(updated=updated_count, fields=updated_fields)
|
||||
|
||||
|
||||
@@ -611,7 +616,11 @@ def set_requirements(call: APICall, company_id, req_model: SetRequirementsReques
|
||||
raise errors.bad_request.MissingTaskFields(
|
||||
"Task has no script field", task=task.id
|
||||
)
|
||||
res = update_task(task, update_cmds=dict(script__requirements=requirements))
|
||||
res = update_task(
|
||||
task,
|
||||
user_id=call.identity.user,
|
||||
update_cmds=dict(script__requirements=requirements),
|
||||
)
|
||||
call.result.data_model = UpdateResponse(updated=res)
|
||||
if res:
|
||||
call.result.data_model.fields = {"script.requirements": requirements}
|
||||
@@ -646,7 +655,9 @@ def update_batch(call: APICall, company_id, _):
|
||||
partial_update_dict = Task.get_safe_update_dict(fields)
|
||||
if not partial_update_dict:
|
||||
continue
|
||||
partial_update_dict.update(last_change=now)
|
||||
partial_update_dict.update(
|
||||
last_change=now, last_changed_by=call.identity.user,
|
||||
)
|
||||
update_op = UpdateOne(
|
||||
{"_id": id, "company": company_id}, {"$set": partial_update_dict}
|
||||
)
|
||||
@@ -710,7 +721,11 @@ def edit(call: APICall, company_id, req_model: UpdateRequest):
|
||||
d.update(value)
|
||||
fields[key] = d
|
||||
|
||||
task_bll.validate(task_bll.create(call, fields))
|
||||
task_bll.validate(
|
||||
task_bll.create(
|
||||
company=call.identity.company, user=call.identity.user, fields=fields
|
||||
)
|
||||
)
|
||||
|
||||
# make sure field names do not end in mongoengine comparison operators
|
||||
fixed_fields = {
|
||||
@@ -719,7 +734,7 @@ def edit(call: APICall, company_id, req_model: UpdateRequest):
|
||||
}
|
||||
if fixed_fields:
|
||||
now = datetime.utcnow()
|
||||
last_change = dict(last_change=now)
|
||||
last_change = dict(last_change=now, last_changed_by=call.identity.user)
|
||||
if not set(fields).issubset(Task.user_set_allowed()):
|
||||
last_change.update(last_update=now)
|
||||
fields.update(**last_change)
|
||||
@@ -734,7 +749,7 @@ def edit(call: APICall, company_id, req_model: UpdateRequest):
|
||||
company_id, project=task.project, fields=fixed_fields
|
||||
)
|
||||
update_project_time(fields.get("project"))
|
||||
unprepare_from_saved(call, fields)
|
||||
conform_task_data(call, fields)
|
||||
call.result.data_model = UpdateResponse(updated=updated, fields=fields)
|
||||
else:
|
||||
call.result.data_model = UpdateResponse(updated=0)
|
||||
@@ -756,6 +771,7 @@ def edit_hyper_params(call: APICall, company_id, request: EditHyperParamsRequest
|
||||
call.result.data = {
|
||||
"updated": HyperParams.edit_params(
|
||||
company_id,
|
||||
user_id=call.identity.user,
|
||||
task_id=request.task,
|
||||
hyperparams=request.hyperparams,
|
||||
replace_hyperparams=request.replace_hyperparams,
|
||||
@@ -769,6 +785,7 @@ def delete_hyper_params(call: APICall, company_id, request: DeleteHyperParamsReq
|
||||
call.result.data = {
|
||||
"deleted": HyperParams.delete_params(
|
||||
company_id,
|
||||
user_id=call.identity.user,
|
||||
task_id=request.task,
|
||||
hyperparams=request.hyperparams,
|
||||
force=request.force,
|
||||
@@ -813,6 +830,7 @@ def edit_configuration(call: APICall, company_id, request: EditConfigurationRequ
|
||||
call.result.data = {
|
||||
"updated": HyperParams.edit_configuration(
|
||||
company_id,
|
||||
user_id=call.identity.user,
|
||||
task_id=request.task,
|
||||
configuration=request.configuration,
|
||||
replace_configuration=request.replace_configuration,
|
||||
@@ -828,6 +846,7 @@ def delete_configuration(
|
||||
call.result.data = {
|
||||
"deleted": HyperParams.delete_configuration(
|
||||
company_id,
|
||||
user_id=call.identity.user,
|
||||
task_id=request.task,
|
||||
configuration=request.configuration,
|
||||
force=request.force,
|
||||
@@ -844,6 +863,7 @@ def enqueue(call: APICall, company_id, request: EnqueueRequest):
|
||||
queued, res = enqueue_task(
|
||||
task_id=request.task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
queue_id=request.queue,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
@@ -868,6 +888,7 @@ def enqueue_many(call: APICall, company_id, request: EnqueueManyRequest):
|
||||
func=partial(
|
||||
enqueue_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
queue_id=request.queue,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
@@ -894,32 +915,34 @@ def enqueue_many(call: APICall, company_id, request: EnqueueManyRequest):
|
||||
|
||||
|
||||
@endpoint(
|
||||
"tasks.dequeue",
|
||||
request_data_model=UpdateRequest,
|
||||
response_data_model=DequeueResponse,
|
||||
"tasks.dequeue", response_data_model=DequeueResponse,
|
||||
)
|
||||
def dequeue(call: APICall, company_id, request: UpdateRequest):
|
||||
def dequeue(call: APICall, company_id, request: DequeueRequest):
|
||||
dequeued, res = dequeue_task(
|
||||
task_id=request.task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
remove_from_all_queues=request.remove_from_all_queues,
|
||||
new_status=request.new_status,
|
||||
)
|
||||
call.result.data_model = DequeueResponse(dequeued=dequeued, **res)
|
||||
|
||||
|
||||
@endpoint(
|
||||
"tasks.dequeue_many",
|
||||
request_data_model=TaskBatchRequest,
|
||||
response_data_model=DequeueManyResponse,
|
||||
"tasks.dequeue_many", response_data_model=DequeueManyResponse,
|
||||
)
|
||||
def dequeue_many(call: APICall, company_id, request: TaskBatchRequest):
|
||||
def dequeue_many(call: APICall, company_id, request: DequeueManyRequest):
|
||||
results, failures = run_batch_operation(
|
||||
func=partial(
|
||||
dequeue_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
remove_from_all_queues=request.remove_from_all_queues,
|
||||
new_status=request.new_status,
|
||||
),
|
||||
ids=request.ids,
|
||||
)
|
||||
@@ -1007,12 +1030,21 @@ def archive(call: APICall, company_id, request: ArchiveRequest):
|
||||
tasks = TaskBLL.assert_exists(
|
||||
company_id,
|
||||
task_ids=request.tasks,
|
||||
only=("id", "execution", "status", "project", "system_tags", "enqueue_status"),
|
||||
only=(
|
||||
"id",
|
||||
"company",
|
||||
"execution",
|
||||
"status",
|
||||
"project",
|
||||
"system_tags",
|
||||
"enqueue_status",
|
||||
),
|
||||
)
|
||||
archived = 0
|
||||
for task in tasks:
|
||||
archived += archive_task(
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
task=task,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
@@ -1031,6 +1063,7 @@ def archive_many(call: APICall, company_id, request: TaskBatchRequest):
|
||||
func=partial(
|
||||
archive_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
),
|
||||
@@ -1052,6 +1085,7 @@ def unarchive_many(call: APICall, company_id, request: TaskBatchRequest):
|
||||
func=partial(
|
||||
unarchive_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
status_message=request.status_message,
|
||||
status_reason=request.status_reason,
|
||||
),
|
||||
@@ -1130,6 +1164,7 @@ def publish(call: APICall, company_id, request: PublishRequest):
|
||||
updates = publish_task(
|
||||
task_id=request.task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force=request.force,
|
||||
publish_model_func=ModelBLL.publish_model if request.publish_model else None,
|
||||
status_reason=request.status_reason,
|
||||
@@ -1148,6 +1183,7 @@ def publish_many(call: APICall, company_id, request: PublishManyRequest):
|
||||
func=partial(
|
||||
publish_task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force=request.force,
|
||||
publish_model_func=ModelBLL.publish_model
|
||||
if request.publish_model
|
||||
@@ -1174,7 +1210,8 @@ def completed(call: APICall, company_id, request: CompletedRequest):
|
||||
res = CompletedResponse(
|
||||
**set_task_status_from_call(
|
||||
request,
|
||||
company_id,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
new_status=TaskStatus.completed,
|
||||
completed=datetime.utcnow(),
|
||||
)
|
||||
@@ -1184,6 +1221,7 @@ def completed(call: APICall, company_id, request: CompletedRequest):
|
||||
publish_res = publish_task(
|
||||
task_id=request.task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
force=request.force,
|
||||
publish_model_func=ModelBLL.publish_model,
|
||||
status_reason=request.status_reason,
|
||||
@@ -1198,9 +1236,12 @@ def completed(call: APICall, company_id, request: CompletedRequest):
|
||||
|
||||
|
||||
@endpoint("tasks.ping", request_data_model=PingRequest)
|
||||
def ping(_, company_id, request: PingRequest):
|
||||
def ping(call: APICall, company_id, request: PingRequest):
|
||||
TaskBLL.set_last_update(
|
||||
task_ids=[request.task], company_id=company_id, last_update=datetime.utcnow()
|
||||
task_ids=[request.task],
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
last_update=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1215,6 +1256,7 @@ def add_or_update_artifacts(
|
||||
call.result.data = {
|
||||
"updated": Artifacts.add_or_update_artifacts(
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
task_id=request.task,
|
||||
artifacts=request.artifacts,
|
||||
force=True,
|
||||
@@ -1231,6 +1273,7 @@ def delete_artifacts(call: APICall, company_id, request: DeleteArtifactsRequest)
|
||||
call.result.data = {
|
||||
"deleted": Artifacts.delete_artifacts(
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
task_id=request.task,
|
||||
artifact_ids=request.artifacts,
|
||||
force=True,
|
||||
@@ -1241,20 +1284,28 @@ def delete_artifacts(call: APICall, company_id, request: DeleteArtifactsRequest)
|
||||
@endpoint("tasks.make_public", min_version="2.9", request_data_model=MakePublicRequest)
|
||||
def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
call.result.data = Task.set_public(
|
||||
company_id, request.ids, invalid_cls=InvalidTaskId, enabled=True
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
ids=request.ids,
|
||||
invalid_cls=InvalidTaskId,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
|
||||
@endpoint("tasks.make_private", min_version="2.9", request_data_model=MakePublicRequest)
|
||||
def make_public(call: APICall, company_id, request: MakePublicRequest):
|
||||
call.result.data = Task.set_public(
|
||||
company_id, request.ids, invalid_cls=InvalidTaskId, enabled=False
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
ids=request.ids,
|
||||
invalid_cls=InvalidTaskId,
|
||||
enabled=False,
|
||||
)
|
||||
|
||||
|
||||
@endpoint("tasks.move", request_data_model=MoveRequest)
|
||||
def move(call: APICall, company_id: str, request: MoveRequest):
|
||||
if not (request.project or request.project_name):
|
||||
if not ("project" in call.data or request.project_name):
|
||||
raise errors.bad_request.MissingRequiredFields(
|
||||
"project or project_name is required"
|
||||
)
|
||||
@@ -1278,8 +1329,21 @@ def move(call: APICall, company_id: str, request: MoveRequest):
|
||||
return {"project_id": project_id}
|
||||
|
||||
|
||||
@endpoint("tasks.update_tags")
|
||||
def update_tags(_, company_id: str, request: UpdateTagsRequest):
|
||||
return {
|
||||
"updated": org_bll.edit_entity_tags(
|
||||
company_id=company_id,
|
||||
entity_cls=Task,
|
||||
entity_ids=request.ids,
|
||||
add_tags=request.add_tags,
|
||||
remove_tags=request.remove_tags,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@endpoint("tasks.add_or_update_model", min_version="2.13")
|
||||
def add_or_update_model(_: APICall, company_id: str, request: AddUpdateModelRequest):
|
||||
def add_or_update_model(call: APICall, company_id: str, request: AddUpdateModelRequest):
|
||||
get_task_for_update(company_id=company_id, task_id=request.task, force=True)
|
||||
|
||||
models_field = f"models__{request.type}"
|
||||
@@ -1290,6 +1354,7 @@ def add_or_update_model(_: APICall, company_id: str, request: AddUpdateModelRequ
|
||||
updated = TaskBLL.update_statistics(
|
||||
task_id=request.task,
|
||||
company_id=company_id,
|
||||
user_id=call.identity.user,
|
||||
last_iteration_max=request.iteration,
|
||||
**({f"push__{models_field}": model} if not updated else {}),
|
||||
)
|
||||
@@ -1298,7 +1363,7 @@ def add_or_update_model(_: APICall, company_id: str, request: AddUpdateModelRequ
|
||||
|
||||
|
||||
@endpoint("tasks.delete_models", min_version="2.13")
|
||||
def delete_models(_: APICall, company_id: str, request: DeleteModelsRequest):
|
||||
def delete_models(call: APICall, company_id: str, request: DeleteModelsRequest):
|
||||
task = get_task_for_update(company_id=company_id, task_id=request.task, force=True)
|
||||
|
||||
delete_names = {
|
||||
@@ -1311,5 +1376,7 @@ def delete_models(_: APICall, company_id: str, request: DeleteModelsRequest):
|
||||
if names
|
||||
}
|
||||
|
||||
updated = task.update(last_change=datetime.utcnow(), **commands,)
|
||||
updated = task.update(
|
||||
last_change=datetime.utcnow(), last_changed_by=call.identity.user, **commands,
|
||||
)
|
||||
return {"updated": updated}
|
||||
|
||||
@@ -98,9 +98,7 @@ def get_current_user(call: APICall, company_id, _):
|
||||
user_id = call.identity.user
|
||||
|
||||
projection = (
|
||||
{"company.name"}
|
||||
.union(User.get_fields())
|
||||
.difference(User.get_exclude_fields())
|
||||
{"company.name"}.union(User.get_fields()).difference(User.get_exclude_fields())
|
||||
)
|
||||
res = User.get_many_with_join(
|
||||
query=Q(id=user_id),
|
||||
@@ -114,9 +112,13 @@ def get_current_user(call: APICall, company_id, _):
|
||||
user = res[0]
|
||||
user["role"] = call.identity.role
|
||||
|
||||
resp = {
|
||||
"user": user,
|
||||
"getting_started": config.get("apiserver.getting_started_info", None),
|
||||
resp = dict(
|
||||
user=user, getting_started=config.get("apiserver.getting_started_info", None)
|
||||
)
|
||||
resp["settings"] = {
|
||||
"max_download_items": int(
|
||||
config.get("services.organization.download.max_download_items", 1000)
|
||||
)
|
||||
}
|
||||
call.result.data = resp
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ def _upgrade_tags(call: APICall, tags: Sequence, system_tags: Sequence):
|
||||
def validate_tags(tags: Sequence[str], system_tags: Sequence[str]):
|
||||
for values in filter(None, (tags, system_tags)):
|
||||
unsupported = [
|
||||
t for t in values if t.startswith(GetMixin.ListFieldBucketHelper.op_prefix)
|
||||
t for t in values if t.startswith(GetMixin.NewListFieldBucketHelper.op_prefix)
|
||||
]
|
||||
if unsupported:
|
||||
raise errors.bad_request.FieldsValueError(
|
||||
|
||||
@@ -22,6 +22,7 @@ from apiserver.apimodels.workers import (
|
||||
GetActivityReportRequest,
|
||||
GetActivityReportResponse,
|
||||
ActivityReportSeries,
|
||||
GetCountRequest,
|
||||
)
|
||||
from apiserver.bll.workers import WorkerBLL
|
||||
from apiserver.config_repo import config
|
||||
@@ -50,12 +51,29 @@ def get_all(call: APICall, company_id: str, request: GetAllRequest):
|
||||
)
|
||||
|
||||
|
||||
@endpoint(
|
||||
"workers.get_count", request_data_model=GetCountRequest,
|
||||
)
|
||||
def get_all(call: APICall, company_id: str, request: GetCountRequest):
|
||||
call.result.data = {
|
||||
"count": worker_bll.get_count(
|
||||
company_id,
|
||||
request.last_seen,
|
||||
tags=request.tags,
|
||||
system_tags=request.system_tags,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@endpoint("workers.register", min_version="2.4", request_data_model=RegisterRequest)
|
||||
def register(call: APICall, company_id, request: RegisterRequest):
|
||||
worker = request.worker
|
||||
timeout = request.timeout
|
||||
queues = request.queues
|
||||
|
||||
if not timeout:
|
||||
timeout = config.get("apiserver.workers.default_timeout", 10 * 60)
|
||||
|
||||
if not timeout or timeout <= 0:
|
||||
raise bad_request.WorkerRegistrationFailed(
|
||||
"invalid timeout", timeout=timeout, worker=worker
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user