mirror of
https://github.com/clearml/clearml-server
synced 2025-06-26 23:15:47 +00:00
Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7b6f24b24d | ||
|
|
d03a931d84 | ||
|
|
5cc7199661 | ||
|
|
6537e9ef69 | ||
|
|
930aaff791 | ||
|
|
1999fb2479 | ||
|
|
9db14cc31d | ||
|
|
e3cc689528 | ||
|
|
9e0adc77dd | ||
|
|
58d9a64537 | ||
|
|
d397d2ae20 | ||
|
|
2d711e1500 | ||
|
|
97992b0d9e | ||
|
|
bc23f1b0cf | ||
|
|
6b3eff1426 | ||
|
|
caaf801cd0 | ||
|
|
c23e8a90d0 | ||
|
|
fa5b28ca0e | ||
|
|
bfb55a9463 | ||
|
|
37e485e1f2 | ||
|
|
3451ff441f | ||
|
|
53c9b5525e | ||
|
|
e5230edac3 | ||
|
|
a54dd8030c | ||
|
|
482a5c34bc | ||
|
|
ee2a72c70f | ||
|
|
a0d8aaf3b9 | ||
|
|
de1f823213 | ||
|
|
0c9e2f92ee | ||
|
|
6c49e96ff0 | ||
|
|
81e3fc6577 | ||
|
|
e6dc4b7557 | ||
|
|
238a47a197 | ||
|
|
04e7076628 | ||
|
|
0531612bf4 | ||
|
|
3ae410a1e9 | ||
|
|
98ed3075dd | ||
|
|
b871bf4224 | ||
|
|
8d4c02fc3c | ||
|
|
b986980c75 | ||
|
|
a4fa567be2 | ||
|
|
ddb91f226a | ||
|
|
7772f47773 | ||
|
|
9c118d14e0 | ||
|
|
efd56e085e | ||
|
|
4dff163af4 | ||
|
|
242a78a0fe | ||
|
|
78989fea91 | ||
|
|
5de7c12062 | ||
|
|
3f79c19079 | ||
|
|
fe29743c54 | ||
|
|
d760cf5835 | ||
|
|
3695f25a5f | ||
|
|
c6f1beafdd | ||
|
|
68a54c34f3 | ||
|
|
ab495ae586 | ||
|
|
b058770af1 | ||
|
|
f7e833bf6f | ||
|
|
36b9ab0453 | ||
|
|
ec0436d0da | ||
|
|
0f6c4e75b7 | ||
|
|
a41ae112a1 | ||
|
|
c28f478ea8 | ||
|
|
c18eb99d06 | ||
|
|
3a60f00d93 | ||
|
|
ee87778548 | ||
|
|
52c0c4d438 | ||
|
|
d117a4f022 | ||
|
|
6683d2d7a9 | ||
|
|
05357fe25e | ||
|
|
adc1825843 | ||
|
|
0c15169668 | ||
|
|
123dc1dcfb | ||
|
|
b2feafac09 | ||
|
|
b41ab8c550 | ||
|
|
62d5779bd5 | ||
|
|
f8b9d9802e | ||
|
|
dd8a1503b0 | ||
|
|
cff98ae900 | ||
|
|
9b108740da | ||
|
|
08a7bc7c9f | ||
|
|
fb256d7e5b | ||
|
|
710443b078 | ||
|
|
e0cde2f7c9 | ||
|
|
60b9c8de14 | ||
|
|
ecffe26be4 | ||
|
|
2570bd9e26 | ||
|
|
174f84514a | ||
|
|
65cb8d7b43 | ||
|
|
5f8ef808a3 | ||
|
|
4941ac70e0 | ||
|
|
67cd461145 | ||
|
|
92b5fc6f9a | ||
|
|
b90165b4e4 | ||
|
|
6c2dcb5c8a | ||
|
|
3efed32934 | ||
|
|
69737308fe | ||
|
|
a6dbea808a | ||
|
|
5131b17901 | ||
|
|
5f21c3a56d | ||
|
|
2350ac64ed | ||
|
|
d146127c18 | ||
|
|
abd65e103e |
@@ -26,6 +26,9 @@
|
||||
23: ["invalid_domain_name", "malformed domain name"]
|
||||
24: ["not_public_object", "object is not public"]
|
||||
|
||||
# Auth / Login
|
||||
75: ["invalid_access_key", "access key not found"]
|
||||
|
||||
# Tasks
|
||||
100: ["task_error", "general task error"]
|
||||
101: ["invalid_task_id", "invalid task id"]
|
||||
@@ -47,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"]
|
||||
@@ -67,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"]
|
||||
@@ -86,7 +97,7 @@
|
||||
|
||||
# Database
|
||||
800: ["data_validation_error", "data validation error"]
|
||||
801: ["expected_unique_data", "value combination already exists"]
|
||||
801: ["expected_unique_data", "value combination already exists (unique field already contains this value)"]
|
||||
|
||||
# Workers
|
||||
1001: ["invalid_worker_id", "invalid worker id"]
|
||||
|
||||
@@ -61,10 +61,11 @@ class ListField(fields.ListField):
|
||||
item.validate()
|
||||
|
||||
|
||||
# since there is no distinction between None and empty DictField
|
||||
# this value can be used as sentinel in order to distinguish
|
||||
# between not set and empty DictField
|
||||
DictFieldNotSet = {}
|
||||
class ScalarField(fields.BaseField):
|
||||
|
||||
"""String field."""
|
||||
|
||||
types = (str, int, float, bool)
|
||||
|
||||
|
||||
class DictField(fields.BaseField):
|
||||
|
||||
@@ -96,6 +96,11 @@ class GetCredentialsResponse(Base):
|
||||
credentials = ListField(CredentialsResponse)
|
||||
|
||||
|
||||
class EditCredentialsRequest(Base):
|
||||
access_key = StringField(required=True)
|
||||
label = StringField()
|
||||
|
||||
|
||||
class RevokeCredentialsRequest(Base):
|
||||
access_key = StringField(required=True)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class MetricVariants(Base):
|
||||
class ScalarMetricsIterHistogramRequest(HistogramRequestBase):
|
||||
task: str = StringField(required=True)
|
||||
metrics: Sequence[MetricVariants] = ListField(items_types=MetricVariants)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class MultiTaskScalarMetricsIterHistogramRequest(HistogramRequestBase):
|
||||
@@ -35,11 +36,12 @@ 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
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class TaskMetric(Base):
|
||||
@@ -48,7 +50,7 @@ class TaskMetric(Base):
|
||||
variants: Sequence[str] = ListField(items_types=str)
|
||||
|
||||
|
||||
class DebugImagesRequest(Base):
|
||||
class MetricEventsRequest(Base):
|
||||
metrics: Sequence[TaskMetric] = ListField(
|
||||
items_types=TaskMetric, validators=[Length(minimum_value=1)]
|
||||
)
|
||||
@@ -56,24 +58,36 @@ class DebugImagesRequest(Base):
|
||||
navigate_earlier: bool = BoolField(default=True)
|
||||
refresh: bool = BoolField(default=False)
|
||||
scroll_id: str = StringField()
|
||||
model_events: bool = BoolField()
|
||||
|
||||
|
||||
class TaskMetricVariant(Base):
|
||||
class GetVariantSampleRequest(Base):
|
||||
task: str = StringField(required=True)
|
||||
metric: str = StringField(required=True)
|
||||
variant: str = StringField(required=True)
|
||||
|
||||
|
||||
class GetDebugImageSampleRequest(TaskMetricVariant):
|
||||
iteration: Optional[int] = IntField()
|
||||
refresh: bool = BoolField(default=False)
|
||||
scroll_id: Optional[str] = StringField()
|
||||
navigate_current_metric: bool = BoolField(default=True)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class NextDebugImageSampleRequest(Base):
|
||||
class GetMetricSamplesRequest(Base):
|
||||
task: str = StringField(required=True)
|
||||
metric: str = StringField(required=True)
|
||||
iteration: Optional[int] = IntField()
|
||||
refresh: bool = BoolField(default=False)
|
||||
scroll_id: Optional[str] = StringField()
|
||||
navigate_current_metric: bool = BoolField(default=True)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class NextHistorySampleRequest(Base):
|
||||
task: str = StringField(required=True)
|
||||
scroll_id: Optional[str] = StringField()
|
||||
navigate_earlier: bool = BoolField(default=True)
|
||||
next_iteration: bool = BoolField(default=False)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class LogOrderEnum(StringEnum):
|
||||
@@ -92,6 +106,7 @@ class TaskEventsRequest(TaskEventsRequestBase):
|
||||
order: Optional[str] = ActualEnumField(LogOrderEnum, default=LogOrderEnum.asc)
|
||||
scroll_id: str = StringField()
|
||||
count_total: bool = BoolField(default=True)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class LogEventsRequest(TaskEventsRequestBase):
|
||||
@@ -107,6 +122,7 @@ class ScalarMetricsIterRawRequest(TaskEventsRequestBase):
|
||||
metric: MetricVariants = EmbeddedField(MetricVariants, required=True)
|
||||
count_total: bool = BoolField(default=False)
|
||||
scroll_id: str = StringField()
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class IterationEvents(Base):
|
||||
@@ -119,21 +135,47 @@ class MetricEvents(Base):
|
||||
iterations: Sequence[IterationEvents] = ListField(items_types=IterationEvents)
|
||||
|
||||
|
||||
class DebugImageResponse(Base):
|
||||
class MetricEventsResponse(Base):
|
||||
metrics: Sequence[MetricEvents] = ListField(items_types=MetricEvents)
|
||||
scroll_id: str = StringField()
|
||||
|
||||
|
||||
class TaskMetricsRequest(Base):
|
||||
class MultiTasksRequestBase(Base):
|
||||
tasks: Sequence[str] = ListField(
|
||||
items_types=str, validators=[Length(minimum_value=1)]
|
||||
)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class SingleValueMetricsRequest(MultiTasksRequestBase):
|
||||
pass
|
||||
|
||||
|
||||
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)
|
||||
scroll_id: str = StringField()
|
||||
no_scroll: bool = BoolField(default=False)
|
||||
metrics: Sequence[MetricVariants] = ListField(items_types=MetricVariants)
|
||||
model_events: bool = BoolField(default=False)
|
||||
|
||||
|
||||
class ClearScrollRequest(Base):
|
||||
scroll_id: str = StringField()
|
||||
|
||||
|
||||
class ClearTaskLogRequest(Base):
|
||||
task: str = StringField(required=True)
|
||||
threshold_sec = IntField()
|
||||
allow_locked = BoolField(default=False)
|
||||
|
||||
@@ -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):
|
||||
@@ -75,3 +77,8 @@ class DeleteMetadataRequest(DeleteMetadata):
|
||||
|
||||
class AddOrUpdateMetadataRequest(AddOrUpdateMetadata):
|
||||
model = fields.StringField(required=True)
|
||||
|
||||
|
||||
class ModelsGetRequest(models.Base):
|
||||
include_stats = fields.BoolField(default=False)
|
||||
allow_public = fields.BoolField(default=True)
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
from enum import auto
|
||||
from typing import Sequence
|
||||
|
||||
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):
|
||||
@@ -9,3 +16,47 @@ class Filter(models.Base):
|
||||
class TagsRequest(models.Base):
|
||||
include_system = fields.BoolField(default=False)
|
||||
filter = fields.EmbeddedField(Filter)
|
||||
|
||||
|
||||
class EntitiesCountRequest(models.Base):
|
||||
projects = DictField()
|
||||
tasks = DictField()
|
||||
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,25 +51,50 @@ 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)
|
||||
|
||||
|
||||
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):
|
||||
include_dataset_stats = fields.BoolField(default=False)
|
||||
include_stats = fields.BoolField(default=False)
|
||||
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)
|
||||
|
||||
@@ -26,9 +26,19 @@ class QueueRequest(Base):
|
||||
queue = StringField(required=True)
|
||||
|
||||
|
||||
class GetByIdRequest(QueueRequest):
|
||||
max_task_entries = IntField()
|
||||
|
||||
|
||||
class GetAllRequest(Base):
|
||||
max_task_entries = IntField()
|
||||
search_hidden = BoolField(default=False)
|
||||
|
||||
|
||||
class GetNextTaskRequest(QueueRequest):
|
||||
queue = StringField(required=True)
|
||||
get_task_info = BoolField(default=False)
|
||||
task = StringField()
|
||||
|
||||
|
||||
class DeleteRequest(QueueRequest):
|
||||
@@ -59,6 +69,7 @@ class GetMetricsRequest(Base):
|
||||
from_date = FloatField(required=True, validators=validators.Min(0))
|
||||
to_date = FloatField(required=True, validators=validators.Min(0))
|
||||
interval = IntField(required=True, validators=validators.Min(1))
|
||||
refresh = BoolField(default=False)
|
||||
|
||||
|
||||
class QueueMetrics(Base):
|
||||
|
||||
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)
|
||||
@@ -42,6 +42,7 @@ class StartedResponse(UpdateResponse):
|
||||
|
||||
class EnqueueResponse(UpdateResponse):
|
||||
queued = IntField()
|
||||
queue_watched = BoolField()
|
||||
|
||||
|
||||
class EnqueueBatchItem(UpdateBatchItem):
|
||||
@@ -50,6 +51,7 @@ class EnqueueBatchItem(UpdateBatchItem):
|
||||
|
||||
class EnqueueManyResponse(BatchResponse):
|
||||
succeeded: Sequence[EnqueueBatchItem] = ListField(EnqueueBatchItem)
|
||||
queue_watched = BoolField()
|
||||
|
||||
|
||||
class DequeueResponse(UpdateResponse):
|
||||
@@ -94,20 +96,36 @@ 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()
|
||||
verify_watched_queue = BoolField(default=False)
|
||||
|
||||
|
||||
class DeleteRequest(UpdateRequest):
|
||||
move_to_trash = BoolField(default=True)
|
||||
return_file_urls = BoolField(default=False)
|
||||
delete_output_models = BoolField(default=True)
|
||||
delete_external_artifacts = BoolField(default=True)
|
||||
|
||||
|
||||
class SetRequirementsRequest(TaskRequest):
|
||||
requirements = DictField(required=True)
|
||||
|
||||
|
||||
class CompletedRequest(UpdateRequest):
|
||||
publish = BoolField(default=False)
|
||||
|
||||
|
||||
class CompletedResponse(UpdateResponse):
|
||||
published = IntField(default=0)
|
||||
|
||||
|
||||
class PublishRequest(UpdateRequest):
|
||||
publish_model = BoolField(default=True)
|
||||
|
||||
@@ -171,6 +189,7 @@ class ResetRequest(UpdateRequest):
|
||||
clear_all = BoolField(default=False)
|
||||
return_file_urls = BoolField(default=False)
|
||||
delete_output_models = BoolField(default=True)
|
||||
delete_external_artifacts = BoolField(default=True)
|
||||
|
||||
|
||||
class MultiTaskRequest(models.Base):
|
||||
@@ -260,9 +279,16 @@ 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()
|
||||
validate_tasks = BoolField(default=False)
|
||||
verify_watched_queue = BoolField(default=False)
|
||||
|
||||
|
||||
class DeleteManyRequest(TaskBatchRequest):
|
||||
@@ -270,6 +296,7 @@ class DeleteManyRequest(TaskBatchRequest):
|
||||
return_file_urls = BoolField(default=False)
|
||||
delete_output_models = BoolField(default=True)
|
||||
force = BoolField(default=False)
|
||||
delete_external_artifacts = BoolField(default=True)
|
||||
|
||||
|
||||
class ResetManyRequest(TaskBatchRequest):
|
||||
@@ -277,6 +304,7 @@ class ResetManyRequest(TaskBatchRequest):
|
||||
return_file_urls = BoolField(default=False)
|
||||
delete_output_models = BoolField(default=True)
|
||||
force = BoolField(default=False)
|
||||
delete_external_artifacts = BoolField(default=True)
|
||||
|
||||
|
||||
class PublishManyRequest(TaskBatchRequest):
|
||||
@@ -300,3 +328,8 @@ 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)
|
||||
|
||||
@@ -12,7 +12,7 @@ from jsonmodels.fields import (
|
||||
)
|
||||
from jsonmodels.models import Base
|
||||
|
||||
from apiserver.apimodels import make_default, ListField, EnumField, JsonSerializableMixin
|
||||
from apiserver.apimodels import ListField, EnumField, JsonSerializableMixin
|
||||
|
||||
DEFAULT_TIMEOUT = 10 * 60
|
||||
|
||||
@@ -20,12 +20,11 @@ DEFAULT_TIMEOUT = 10 * 60
|
||||
class WorkerRequest(Base):
|
||||
worker = StringField(required=True)
|
||||
tags = ListField(str)
|
||||
system_tags = ListField(str)
|
||||
|
||||
|
||||
class RegisterRequest(WorkerRequest):
|
||||
timeout = make_default(
|
||||
IntField, DEFAULT_TIMEOUT
|
||||
)() # registration timeout in seconds (default is 10min)
|
||||
timeout = IntField(default=0) # registration timeout in seconds (if not specified, default is 10min)
|
||||
queues = ListField(six.string_types) # list of queues this worker listens to
|
||||
|
||||
|
||||
@@ -76,6 +75,7 @@ class WorkerEntry(Base, JsonSerializableMixin):
|
||||
last_activity_time = DateTimeField(required=True)
|
||||
last_report_time = DateTimeField()
|
||||
tags = ListField(str)
|
||||
system_tags = ListField(str)
|
||||
|
||||
|
||||
class CurrentTaskEntry(IdNameEntry):
|
||||
@@ -96,12 +96,18 @@ class WorkerResponseEntry(WorkerEntry):
|
||||
|
||||
class GetAllRequest(Base):
|
||||
last_seen = IntField(default=3600)
|
||||
tags = ListField(str)
|
||||
system_tags = ListField(str)
|
||||
|
||||
|
||||
class GetAllResponse(Base):
|
||||
workers = ListField(WorkerResponseEntry)
|
||||
|
||||
|
||||
class GetCountRequest(GetAllRequest):
|
||||
last_seen = IntField(default=0)
|
||||
|
||||
|
||||
class StatsBase(Base):
|
||||
worker_ids = ListField(str)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class AuthBLL:
|
||||
feature_set="basic",
|
||||
)
|
||||
|
||||
return GetTokenResponse(token=token.decode("ascii"))
|
||||
return GetTokenResponse(token=token)
|
||||
|
||||
@staticmethod
|
||||
def create_user(request: CreateUserRequest, call: APICall = None) -> str:
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
import operator
|
||||
from typing import Sequence, Tuple, Optional
|
||||
|
||||
import attr
|
||||
from boltons.iterutils import first
|
||||
from elasticsearch import Elasticsearch
|
||||
from jsonmodels.fields import StringField, ListField, IntField, BoolField
|
||||
from jsonmodels.models import Base
|
||||
from redis import StrictRedis
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels import JsonSerializableMixin
|
||||
from apiserver.bll.event.event_common import (
|
||||
EventSettings,
|
||||
EventType,
|
||||
check_empty_data,
|
||||
search_company_events,
|
||||
)
|
||||
from apiserver.bll.redis_cache_manager import RedisCacheManager
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.utilities.dicts import nested_get
|
||||
|
||||
|
||||
class VariantState(Base):
|
||||
name: str = StringField(required=True)
|
||||
min_iteration: int = IntField()
|
||||
max_iteration: int = IntField()
|
||||
|
||||
|
||||
class DebugSampleHistoryState(Base, JsonSerializableMixin):
|
||||
id: str = StringField(required=True)
|
||||
iteration: int = IntField()
|
||||
variant: str = StringField()
|
||||
task: str = StringField()
|
||||
metric: str = StringField()
|
||||
reached_first: bool = BoolField()
|
||||
reached_last: bool = BoolField()
|
||||
variant_states: Sequence[VariantState] = ListField([VariantState])
|
||||
warning: str = StringField()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class DebugSampleHistoryResult(object):
|
||||
scroll_id: str = None
|
||||
event: dict = None
|
||||
min_iteration: int = None
|
||||
max_iteration: int = None
|
||||
|
||||
|
||||
class DebugSampleHistory:
|
||||
EVENT_TYPE = EventType.metrics_image
|
||||
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch):
|
||||
self.es = es
|
||||
self.cache_manager = RedisCacheManager(
|
||||
state_class=DebugSampleHistoryState,
|
||||
redis=redis,
|
||||
expiration_interval=EventSettings.state_expiration_sec,
|
||||
)
|
||||
|
||||
def get_next_debug_image(
|
||||
self, company_id: str, task: str, state_id: str, navigate_earlier: bool
|
||||
) -> DebugSampleHistoryResult:
|
||||
"""
|
||||
Get the debug image for next/prev variant on the current iteration
|
||||
If does not exist then try getting image for the first/last variant from next/prev iteration
|
||||
"""
|
||||
res = DebugSampleHistoryResult(scroll_id=state_id)
|
||||
state = self.cache_manager.get_state(state_id)
|
||||
if not state or state.task != task:
|
||||
raise errors.bad_request.InvalidScrollId(scroll_id=state_id)
|
||||
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=self.EVENT_TYPE):
|
||||
return res
|
||||
|
||||
image = self._get_next_for_current_iteration(
|
||||
company_id=company_id, navigate_earlier=navigate_earlier, state=state
|
||||
) or self._get_next_for_another_iteration(
|
||||
company_id=company_id, navigate_earlier=navigate_earlier, state=state
|
||||
)
|
||||
if not image:
|
||||
return res
|
||||
|
||||
self._fill_res_and_update_state(image=image, res=res, state=state)
|
||||
self.cache_manager.set_state(state=state)
|
||||
return res
|
||||
|
||||
def _fill_res_and_update_state(
|
||||
self, image: dict, res: DebugSampleHistoryResult, state: DebugSampleHistoryState
|
||||
):
|
||||
state.variant = image["variant"]
|
||||
state.iteration = image["iter"]
|
||||
res.event = image
|
||||
var_state = first(s for s in state.variant_states if s.name == state.variant)
|
||||
if var_state:
|
||||
res.min_iteration = var_state.min_iteration
|
||||
res.max_iteration = var_state.max_iteration
|
||||
|
||||
def _get_next_for_current_iteration(
|
||||
self, company_id: str, navigate_earlier: bool, state: DebugSampleHistoryState
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Get the image for next (if navigated earlier is False) or previous variant sorted by name for the same iteration
|
||||
Only variants for which the iteration falls into their valid range are considered
|
||||
Return None if no such variant or image is found
|
||||
"""
|
||||
cmp = operator.lt if navigate_earlier else operator.gt
|
||||
variants = [
|
||||
var_state
|
||||
for var_state in state.variant_states
|
||||
if cmp(var_state.name, state.variant)
|
||||
and var_state.min_iteration <= state.iteration
|
||||
]
|
||||
if not variants:
|
||||
return
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": state.task}},
|
||||
{"term": {"metric": state.metric}},
|
||||
{"terms": {"variant": [v.name for v in variants]}},
|
||||
{"term": {"iter": state.iteration}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
es_req = {
|
||||
"size": 1,
|
||||
"sort": {"variant": "desc" if navigate_earlier else "asc"},
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext(
|
||||
"es", "get_next_for_current_iteration"
|
||||
):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=self.EVENT_TYPE, body=es_req
|
||||
)
|
||||
|
||||
hits = nested_get(es_res, ("hits", "hits"))
|
||||
if not hits:
|
||||
return
|
||||
|
||||
return hits[0]["_source"]
|
||||
|
||||
def _get_next_for_another_iteration(
|
||||
self, company_id: str, navigate_earlier: bool, state: DebugSampleHistoryState
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Get the image for the first variant for the next iteration (if navigate_earlier is set to False)
|
||||
or from the last variant for the previous iteration (otherwise)
|
||||
The variants for which the image falls in invalid range are discarded
|
||||
If no suitable image is found then None is returned
|
||||
"""
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": state.task}},
|
||||
{"term": {"metric": state.metric}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
|
||||
if navigate_earlier:
|
||||
range_operator = "lt"
|
||||
order = "desc"
|
||||
variants = [
|
||||
var_state
|
||||
for var_state in state.variant_states
|
||||
if var_state.min_iteration < state.iteration
|
||||
]
|
||||
else:
|
||||
range_operator = "gt"
|
||||
order = "asc"
|
||||
variants = state.variant_states
|
||||
|
||||
if not variants:
|
||||
return
|
||||
|
||||
variants_conditions = [
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"variant": v.name}},
|
||||
{"range": {"iter": {"gte": v.min_iteration}}},
|
||||
]
|
||||
}
|
||||
}
|
||||
for v in variants
|
||||
]
|
||||
must_conditions.append({"bool": {"should": variants_conditions}})
|
||||
must_conditions.append({"range": {"iter": {range_operator: state.iteration}}},)
|
||||
|
||||
es_req = {
|
||||
"size": 1,
|
||||
"sort": [{"iter": order}, {"variant": order}],
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
}
|
||||
with translate_errors_context(), TimingContext(
|
||||
"es", "get_next_for_another_iteration"
|
||||
):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=self.EVENT_TYPE, body=es_req
|
||||
)
|
||||
|
||||
hits = nested_get(es_res, ("hits", "hits"))
|
||||
if not hits:
|
||||
return
|
||||
|
||||
return hits[0]["_source"]
|
||||
|
||||
def get_debug_image_for_variant(
|
||||
self,
|
||||
company_id: str,
|
||||
task: str,
|
||||
metric: str,
|
||||
variant: str,
|
||||
iteration: Optional[int] = None,
|
||||
refresh: bool = False,
|
||||
state_id: str = None,
|
||||
) -> DebugSampleHistoryResult:
|
||||
"""
|
||||
Get the debug image for the requested iteration or the latest before it
|
||||
If the iteration is not passed then get the latest event
|
||||
"""
|
||||
res = DebugSampleHistoryResult()
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=self.EVENT_TYPE):
|
||||
return res
|
||||
|
||||
def init_state(state_: DebugSampleHistoryState):
|
||||
state_.task = task
|
||||
state_.metric = metric
|
||||
self._reset_variant_states(company_id=company_id, state=state_)
|
||||
|
||||
def validate_state(state_: DebugSampleHistoryState):
|
||||
if state_.task != task or state_.metric != metric:
|
||||
raise errors.bad_request.InvalidScrollId(
|
||||
"Task and metric stored in the state do not match the passed ones",
|
||||
scroll_id=state_.id,
|
||||
)
|
||||
if refresh:
|
||||
self._reset_variant_states(company_id=company_id, state=state_)
|
||||
|
||||
state: DebugSampleHistoryState
|
||||
with self.cache_manager.get_or_create_state(
|
||||
state_id=state_id, init_state=init_state, validate_state=validate_state,
|
||||
) as state:
|
||||
res.scroll_id = state.id
|
||||
|
||||
var_state = first(s for s in state.variant_states if s.name == variant)
|
||||
if not var_state:
|
||||
return res
|
||||
|
||||
res.min_iteration = var_state.min_iteration
|
||||
res.max_iteration = var_state.max_iteration
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": task}},
|
||||
{"term": {"metric": metric}},
|
||||
{"term": {"variant": variant}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
if iteration is not None:
|
||||
must_conditions.append(
|
||||
{
|
||||
"range": {
|
||||
"iter": {"lte": iteration, "gte": var_state.min_iteration}
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
must_conditions.append(
|
||||
{"range": {"iter": {"gte": var_state.min_iteration}}}
|
||||
)
|
||||
|
||||
es_req = {
|
||||
"size": 1,
|
||||
"sort": {"iter": "desc"},
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext(
|
||||
"es", "get_debug_image_for_variant"
|
||||
):
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=self.EVENT_TYPE,
|
||||
body=es_req,
|
||||
)
|
||||
|
||||
hits = nested_get(es_res, ("hits", "hits"))
|
||||
if not hits:
|
||||
return res
|
||||
|
||||
self._fill_res_and_update_state(
|
||||
image=hits[0]["_source"], res=res, state=state
|
||||
)
|
||||
return res
|
||||
|
||||
def _reset_variant_states(self, company_id: str, state: DebugSampleHistoryState):
|
||||
variant_iterations = self._get_variant_iterations(
|
||||
company_id=company_id, task=state.task, metric=state.metric
|
||||
)
|
||||
state.variant_states = [
|
||||
VariantState(name=var_name, min_iteration=min_iter, max_iteration=max_iter)
|
||||
for var_name, min_iter, max_iter in variant_iterations
|
||||
]
|
||||
|
||||
def _get_variant_iterations(
|
||||
self,
|
||||
company_id: str,
|
||||
task: str,
|
||||
metric: str,
|
||||
variants: Optional[Sequence[str]] = None,
|
||||
) -> Sequence[Tuple[str, int, int]]:
|
||||
"""
|
||||
Return valid min and max iterations that the task reported images
|
||||
The min iteration is the lowest iteration that contains non-recycled image url
|
||||
"""
|
||||
must = [
|
||||
{"term": {"task": task}},
|
||||
{"term": {"metric": metric}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
if variants:
|
||||
must.append({"terms": {"variant": variants}})
|
||||
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"query": {"bool": {"must": must}},
|
||||
"aggs": {
|
||||
"variants": {
|
||||
# all variants that sent debug images
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": EventSettings.max_variants_count,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"last_iter": {"max": {"field": "iter"}},
|
||||
"urls": {
|
||||
# group by urls and choose the minimal iteration
|
||||
# from all the maximal iterations per url
|
||||
"terms": {
|
||||
"field": "url",
|
||||
"order": {"max_iter": "asc"},
|
||||
"size": 1,
|
||||
},
|
||||
"aggs": {
|
||||
# find max iteration for each url
|
||||
"max_iter": {"max": {"field": "iter"}}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext(
|
||||
"es", "get_debug_image_iterations"
|
||||
):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=self.EVENT_TYPE, body=es_req
|
||||
)
|
||||
|
||||
def get_variant_data(variant_bucket: dict) -> Tuple[str, int, int]:
|
||||
variant = variant_bucket["key"]
|
||||
urls = nested_get(variant_bucket, ("urls", "buckets"))
|
||||
min_iter = int(urls[0]["max_iter"]["value"])
|
||||
max_iter = int(variant_bucket["last_iter"]["value"])
|
||||
return variant, min_iter, max_iter
|
||||
|
||||
return [
|
||||
get_variant_data(variant_bucket)
|
||||
for variant_bucket in nested_get(
|
||||
es_res, ("aggregations", "variants", "buckets")
|
||||
)
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,15 @@
|
||||
import base64
|
||||
import zlib
|
||||
from enum import Enum
|
||||
from typing import Union, Sequence, Mapping
|
||||
from typing import Union, Sequence, Mapping, Tuple
|
||||
|
||||
from boltons.typeutils import classproperty
|
||||
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
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
@@ -16,10 +21,14 @@ class EventType(Enum):
|
||||
all = "*"
|
||||
|
||||
|
||||
SINGLE_SCALAR_ITERATION = -(2 ** 31)
|
||||
MetricVariants = Mapping[str, Sequence[str]]
|
||||
TaskCompanies = Mapping[str, Sequence[Task]]
|
||||
|
||||
|
||||
class EventSettings:
|
||||
_max_es_allowed_aggregation_buckets = 10000
|
||||
|
||||
@classproperty
|
||||
def max_workers(self):
|
||||
return config.get("services.events.events_retrieval.max_metrics_concurrency", 4)
|
||||
@@ -31,22 +40,31 @@ class EventSettings:
|
||||
)
|
||||
|
||||
@classproperty
|
||||
def max_metrics_count(self):
|
||||
return config.get("services.events.events_retrieval.max_metrics_count", 100)
|
||||
|
||||
@classproperty
|
||||
def max_variants_count(self):
|
||||
return config.get("services.events.events_retrieval.max_variants_count", 100)
|
||||
def max_es_buckets(self):
|
||||
percentage = (
|
||||
min(
|
||||
100,
|
||||
config.get(
|
||||
"services.events.events_retrieval.dynamic_metrics_count_threshold",
|
||||
80,
|
||||
),
|
||||
)
|
||||
/ 100
|
||||
)
|
||||
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}"
|
||||
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
|
||||
|
||||
@@ -66,9 +84,7 @@ def delete_company_events(
|
||||
es: Elasticsearch, company_id: str, event_type: EventType, body: dict, **kwargs
|
||||
) -> dict:
|
||||
es_index = get_index_name(company_id, event_type.value)
|
||||
return es.delete_by_query(
|
||||
index=es_index, body=body, conflicts="proceed", **kwargs
|
||||
)
|
||||
return es.delete_by_query(index=es_index, body=body, conflicts="proceed", **kwargs)
|
||||
|
||||
|
||||
def count_company_events(
|
||||
@@ -78,6 +94,44 @@ def count_company_events(
|
||||
return es.count(index=es_index, body=body, **kwargs)
|
||||
|
||||
|
||||
def get_max_metric_and_variant_counts(
|
||||
es: Elasticsearch,
|
||||
company_id: Union[str, Sequence[str]],
|
||||
event_type: EventType,
|
||||
query: dict,
|
||||
**kwargs,
|
||||
) -> Tuple[int, int]:
|
||||
dynamic = config.get(
|
||||
"services.events.events_retrieval.dynamic_metrics_count", False
|
||||
)
|
||||
max_metrics_count = config.get(
|
||||
"services.events.events_retrieval.max_metrics_count", 100
|
||||
)
|
||||
max_variants_count = config.get(
|
||||
"services.events.events_retrieval.max_variants_count", 100
|
||||
)
|
||||
if not dynamic:
|
||||
return max_metrics_count, max_variants_count
|
||||
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
"aggs": {"metrics_count": {"cardinality": {"field": "metric"}}},
|
||||
}
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
es, company_id=company_id, event_type=event_type, body=es_req, **kwargs,
|
||||
)
|
||||
|
||||
metrics_count = safe_get(
|
||||
es_res, "aggregations/metrics_count/value", max_metrics_count
|
||||
)
|
||||
if not metrics_count:
|
||||
return max_metrics_count, max_variants_count
|
||||
|
||||
return metrics_count, int(EventSettings.max_es_buckets / metrics_count)
|
||||
|
||||
|
||||
def get_metric_variants_condition(metric_variants: MetricVariants,) -> Sequence:
|
||||
conditions = [
|
||||
{
|
||||
@@ -94,3 +148,19 @@ def get_metric_variants_condition(metric_variants: MetricVariants,) -> Sequence:
|
||||
]
|
||||
|
||||
return {"bool": {"should": conditions}}
|
||||
|
||||
|
||||
class PlotFields:
|
||||
valid_plot = "valid_plot"
|
||||
plot_len = "plot_len"
|
||||
plot_str = "plot_str"
|
||||
plot_data = "plot_data"
|
||||
source_urls = "source_urls"
|
||||
|
||||
|
||||
def uncompress_plot(event: dict):
|
||||
plot_data = event.pop(PlotFields.plot_data, None)
|
||||
if plot_data and event.get(PlotFields.plot_str) is None:
|
||||
event[PlotFields.plot_str] = zlib.decompress(
|
||||
base64.b64decode(plot_data)
|
||||
).decode()
|
||||
|
||||
@@ -4,12 +4,11 @@ from collections import defaultdict
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from functools import partial
|
||||
from operator import itemgetter
|
||||
from typing import Sequence, Tuple
|
||||
from typing import Sequence, Tuple, Mapping
|
||||
|
||||
from boltons.iterutils import bucketize
|
||||
from elasticsearch import Elasticsearch
|
||||
from mongoengine import Q
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.bll.event.event_common import (
|
||||
EventType,
|
||||
EventSettings,
|
||||
@@ -17,12 +16,13 @@ from apiserver.bll.event.event_common import (
|
||||
check_empty_data,
|
||||
MetricVariants,
|
||||
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.timing_context import TimingContext
|
||||
from apiserver.tools import safe_get
|
||||
|
||||
log = config.logger(__file__)
|
||||
@@ -108,57 +108,51 @@ class EventMetrics:
|
||||
|
||||
def compare_scalar_metrics_average_per_iter(
|
||||
self,
|
||||
company_id,
|
||||
task_ids: Sequence[str],
|
||||
companies: TaskCompanies,
|
||||
samples,
|
||||
key: ScalarKeyEnum,
|
||||
allow_public=True,
|
||||
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
|
||||
"""
|
||||
task_name_by_id = {}
|
||||
with translate_errors_context():
|
||||
task_objs = Task.get_many(
|
||||
company=company_id,
|
||||
query=Q(id__in=task_ids),
|
||||
allow_public=allow_public,
|
||||
override_projection=("id", "name", "company", "company_origin"),
|
||||
return_dicts=False,
|
||||
)
|
||||
if len(task_objs) < len(task_ids):
|
||||
invalid = tuple(set(task_ids) - set(r.id for r in task_objs))
|
||||
raise errors.bad_request.InvalidTaskId(company=company_id, ids=invalid)
|
||||
task_name_by_id = {t.id: t.name for t in task_objs}
|
||||
|
||||
companies = {t.get_index_company() for t in task_objs}
|
||||
if len(companies) > 1:
|
||||
raise errors.bad_request.InvalidTaskId(
|
||||
"only tasks from the same company are supported"
|
||||
)
|
||||
|
||||
event_type = EventType.metrics_scalar
|
||||
company_id = next(iter(companies))
|
||||
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 {}
|
||||
|
||||
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, 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
|
||||
@@ -166,6 +160,67 @@ class EventMetrics:
|
||||
|
||||
return res
|
||||
|
||||
def get_task_single_value_metrics(
|
||||
self, companies: TaskCompanies
|
||||
) -> Mapping[str, dict]:
|
||||
"""
|
||||
For the requested tasks return all the events delivered for the single iteration (-2**31)
|
||||
"""
|
||||
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 {}
|
||||
|
||||
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 {
|
||||
field: event.get(field)
|
||||
for field in ("metric", "variant", "value", "timestamp")
|
||||
}
|
||||
|
||||
return {
|
||||
task: [_get_value(e) for e in events]
|
||||
for task, events in bucketize(task_events, itemgetter("task")).items()
|
||||
}
|
||||
|
||||
def _get_task_single_value_metrics(
|
||||
self, tasks: Tuple[str, Sequence[str]]
|
||||
) -> Sequence[dict]:
|
||||
company_id, task_ids = tasks
|
||||
es_req = {
|
||||
"size": 10000,
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{"terms": {"task": task_ids}},
|
||||
{"term": {"iter": SINGLE_SCALAR_ITERATION}},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(
|
||||
body=es_req,
|
||||
es=self.es,
|
||||
company_id=company_id,
|
||||
event_type=EventType.metrics_scalar,
|
||||
)
|
||||
if not es_res["hits"]["total"]["value"]:
|
||||
return []
|
||||
|
||||
return [hit["_source"] for hit in es_res["hits"]["hits"]]
|
||||
|
||||
MetricInterval = Tuple[str, str, int, int]
|
||||
MetricIntervalGroup = Tuple[int, Sequence[Tuple[str, str]]]
|
||||
|
||||
@@ -219,11 +274,15 @@ class EventMetrics:
|
||||
Return the list og metric variant intervals as the following tuple:
|
||||
(metric, variant, interval, samples)
|
||||
"""
|
||||
must = [{"term": {"task": task_id}}]
|
||||
must = self._task_conditions(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,
|
||||
)
|
||||
max_variants = int(max_variants // 2)
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
@@ -231,14 +290,14 @@ class EventMetrics:
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": EventSettings.max_metrics_count,
|
||||
"size": max_metrics,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"variants": {
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": EventSettings.max_variants_count,
|
||||
"size": max_variants,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
@@ -252,10 +311,7 @@ class EventMetrics:
|
||||
},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "task_stats_get_interval"):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req,
|
||||
)
|
||||
es_res = search_company_events(body=es_req, **search_args)
|
||||
|
||||
aggs_result = es_res.get("aggregations")
|
||||
if not aggs_result:
|
||||
@@ -307,33 +363,40 @@ class EventMetrics:
|
||||
"""
|
||||
interval, metrics = metrics_interval
|
||||
aggregation = self._add_aggregation_average(key.get_aggregation(interval))
|
||||
aggs = {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": EventSettings.max_metrics_count,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"variants": {
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": EventSettings.max_variants_count,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": aggregation,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
aggs_result = self._query_aggregation_for_task_metrics(
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
aggs=aggs,
|
||||
task_id=task_id,
|
||||
metrics=metrics,
|
||||
query = self._get_task_metrics_query(task_id=task_id, metrics=metrics)
|
||||
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,
|
||||
)
|
||||
max_variants = int(max_variants // 2)
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": max_metrics,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"variants": {
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": max_variants,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": aggregation,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(body=es_req, **search_args)
|
||||
|
||||
aggs_result = es_res.get("aggregations")
|
||||
if not aggs_result:
|
||||
return {}
|
||||
|
||||
@@ -360,19 +423,18 @@ class EventMetrics:
|
||||
for key, value in aggregation.items()
|
||||
}
|
||||
|
||||
def _query_aggregation_for_task_metrics(
|
||||
self,
|
||||
company_id: str,
|
||||
event_type: EventType,
|
||||
aggs: dict,
|
||||
task_id: str,
|
||||
metrics: Sequence[Tuple[str, str]],
|
||||
) -> dict:
|
||||
"""
|
||||
Return the result of elastic search query for the given aggregation filtered
|
||||
by the given task_ids and metrics
|
||||
"""
|
||||
must = [{"term": {"task": task_id}}]
|
||||
@staticmethod
|
||||
def _task_conditions(task_id: str) -> list:
|
||||
return [
|
||||
{"term": {"task": task_id}},
|
||||
{"range": {"iter": {"gt": SINGLE_SCALAR_ITERATION}}},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _get_task_metrics_query(
|
||||
cls, task_id: str, metrics: Sequence[Tuple[str, str]],
|
||||
):
|
||||
must = cls._task_conditions(task_id)
|
||||
if metrics:
|
||||
should = [
|
||||
{
|
||||
@@ -387,20 +449,9 @@ class EventMetrics:
|
||||
]
|
||||
must.append({"bool": {"should": should}})
|
||||
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": {"bool": {"must": must}},
|
||||
"aggs": aggs,
|
||||
}
|
||||
return {"bool": {"must": must}}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "task_stats_scalar"):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req,
|
||||
)
|
||||
|
||||
return es_res.get("aggregations")
|
||||
|
||||
def get_tasks_metrics(
|
||||
def get_task_metrics(
|
||||
self, company_id, task_ids: Sequence, event_type: EventType
|
||||
) -> Sequence:
|
||||
"""
|
||||
@@ -426,22 +477,21 @@ class EventMetrics:
|
||||
) -> Sequence:
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": {"bool": {"must": [{"term": {"task": task_id}}]}},
|
||||
"query": {"bool": {"must": self._task_conditions(task_id)}},
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": EventSettings.max_metrics_count,
|
||||
"size": EventSettings.max_es_buckets,
|
||||
"order": {"_key": "asc"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "_get_task_metrics"):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req
|
||||
)
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req
|
||||
)
|
||||
|
||||
return [
|
||||
metric["key"]
|
||||
|
||||
@@ -4,6 +4,7 @@ import attr
|
||||
import jsonmodels.models
|
||||
import jwt
|
||||
from elasticsearch import Elasticsearch
|
||||
from jwt.algorithms import get_default_algorithms
|
||||
|
||||
from apiserver.bll.event.event_common import (
|
||||
check_empty_data,
|
||||
@@ -16,7 +17,6 @@ from apiserver.bll.event.event_common import (
|
||||
from apiserver.bll.event.scalar_key import ScalarKeyEnum, ScalarKey
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.timing_context import TimingContext
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@@ -67,18 +67,17 @@ class EventsIterator:
|
||||
task_id: 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)
|
||||
es_req = {
|
||||
"query": query,
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "count_task_events"):
|
||||
with translate_errors_context():
|
||||
es_result = count_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
routing=task_id,
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req,
|
||||
)
|
||||
|
||||
return es_result["count"]
|
||||
@@ -113,13 +112,9 @@ class EventsIterator:
|
||||
if from_key_value:
|
||||
es_req["search_after"] = [from_key_value]
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "get_task_events"):
|
||||
with translate_errors_context():
|
||||
es_result = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
routing=task_id,
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req,
|
||||
)
|
||||
hits = es_result["hits"]["hits"]
|
||||
hits_total = es_result["hits"]["total"]["value"]
|
||||
@@ -139,11 +134,7 @@ class EventsIterator:
|
||||
},
|
||||
}
|
||||
es_result = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=event_type,
|
||||
body=es_req,
|
||||
routing=task_id,
|
||||
self.es, company_id=company_id, event_type=event_type, body=es_req,
|
||||
)
|
||||
last_second_hits = es_result["hits"]["hits"]
|
||||
if not last_second_hits or len(last_second_hits) < 2:
|
||||
@@ -188,7 +179,7 @@ class Scroll(jsonmodels.models.Base):
|
||||
key=config.get(
|
||||
"services.events.events_retrieval.scroll_id_key", "1234567890"
|
||||
),
|
||||
).decode()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_scroll_id(cls, scroll_id: str):
|
||||
@@ -199,6 +190,7 @@ class Scroll(jsonmodels.models.Base):
|
||||
key=config.get(
|
||||
"services.events.events_retrieval.scroll_id_key", "1234567890"
|
||||
),
|
||||
algorithms=get_default_algorithms(),
|
||||
)
|
||||
)
|
||||
except jwt.PyJWTError:
|
||||
|
||||
455
apiserver/bll/event/history_debug_image_iterator.py
Normal file
455
apiserver/bll/event/history_debug_image_iterator.py
Normal file
@@ -0,0 +1,455 @@
|
||||
import operator
|
||||
from operator import attrgetter
|
||||
from typing import Sequence, Tuple, Optional, Mapping
|
||||
|
||||
import attr
|
||||
from boltons.iterutils import first, bucketize
|
||||
from elasticsearch import Elasticsearch
|
||||
from jsonmodels.fields import StringField, IntField, BoolField, ListField
|
||||
from jsonmodels.models import Base
|
||||
from redis.client import StrictRedis
|
||||
|
||||
from apiserver.utilities.dicts import nested_get
|
||||
from .event_common import (
|
||||
EventType,
|
||||
EventSettings,
|
||||
check_empty_data,
|
||||
search_company_events,
|
||||
get_max_metric_and_variant_counts,
|
||||
)
|
||||
from apiserver.apimodels import JsonSerializableMixin
|
||||
from apiserver.bll.redis_cache_manager import RedisCacheManager
|
||||
from apiserver.apierrors import errors
|
||||
|
||||
|
||||
class VariantState(Base):
|
||||
name: str = StringField(required=True)
|
||||
metric: str = StringField(default=None)
|
||||
min_iteration: int = IntField()
|
||||
max_iteration: int = IntField()
|
||||
|
||||
|
||||
class DebugImageSampleState(Base, JsonSerializableMixin):
|
||||
id: str = StringField(required=True)
|
||||
iteration: int = IntField()
|
||||
variant: str = StringField()
|
||||
task: str = StringField()
|
||||
metric: str = StringField()
|
||||
variant_states: Sequence[VariantState] = ListField([VariantState])
|
||||
warning: str = StringField()
|
||||
navigate_current_metric = BoolField(default=True)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class VariantSampleResult(object):
|
||||
scroll_id: str = None
|
||||
event: dict = None
|
||||
min_iteration: int = None
|
||||
max_iteration: int = None
|
||||
|
||||
|
||||
class HistoryDebugImageIterator:
|
||||
event_type = EventType.metrics_image
|
||||
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch):
|
||||
self.es = es
|
||||
self.cache_manager = RedisCacheManager(
|
||||
state_class=DebugImageSampleState,
|
||||
redis=redis,
|
||||
expiration_interval=EventSettings.state_expiration_sec,
|
||||
)
|
||||
|
||||
def get_next_sample(
|
||||
self,
|
||||
company_id: str,
|
||||
task: str,
|
||||
state_id: str,
|
||||
navigate_earlier: bool,
|
||||
next_iteration: bool,
|
||||
) -> VariantSampleResult:
|
||||
"""
|
||||
Get the sample for next/prev variant on the current iteration
|
||||
If does not exist then try getting sample for the first/last variant from next/prev iteration
|
||||
"""
|
||||
res = VariantSampleResult(scroll_id=state_id)
|
||||
state = self.cache_manager.get_state(state_id)
|
||||
if not state or state.task != task:
|
||||
raise errors.bad_request.InvalidScrollId(scroll_id=state_id)
|
||||
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=self.event_type):
|
||||
return res
|
||||
|
||||
if next_iteration:
|
||||
event = self._get_next_for_another_iteration(
|
||||
company_id=company_id, navigate_earlier=navigate_earlier, state=state
|
||||
)
|
||||
else:
|
||||
# noinspection PyArgumentList
|
||||
event = first(
|
||||
f(company_id=company_id, navigate_earlier=navigate_earlier, state=state)
|
||||
for f in (
|
||||
self._get_next_for_current_iteration,
|
||||
self._get_next_for_another_iteration,
|
||||
)
|
||||
)
|
||||
if not event:
|
||||
return res
|
||||
|
||||
self._fill_res_and_update_state(event=event, res=res, state=state)
|
||||
self.cache_manager.set_state(state=state)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _fill_res_and_update_state(
|
||||
event: dict, res: VariantSampleResult, state: DebugImageSampleState
|
||||
):
|
||||
state.variant = event["variant"]
|
||||
state.metric = event["metric"]
|
||||
state.iteration = event["iter"]
|
||||
res.event = event
|
||||
var_state = first(
|
||||
vs
|
||||
for vs in state.variant_states
|
||||
if vs.name == state.variant and vs.metric == state.metric
|
||||
)
|
||||
if var_state:
|
||||
res.min_iteration = var_state.min_iteration
|
||||
res.max_iteration = var_state.max_iteration
|
||||
|
||||
@staticmethod
|
||||
def _get_metric_conditions(variants: Sequence[VariantState]) -> dict:
|
||||
metrics = bucketize(variants, key=attrgetter("metric"))
|
||||
|
||||
def _get_variants_conditions(metric_variants: Sequence[VariantState]) -> dict:
|
||||
variants_conditions = [
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"variant": v.name}},
|
||||
{"range": {"iter": {"gte": v.min_iteration}}},
|
||||
]
|
||||
}
|
||||
}
|
||||
for v in metric_variants
|
||||
]
|
||||
return {"bool": {"should": variants_conditions}}
|
||||
|
||||
metrics_conditions = [
|
||||
{
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"metric": metric}},
|
||||
_get_variants_conditions(metric_variants),
|
||||
]
|
||||
}
|
||||
}
|
||||
for metric, metric_variants in metrics.items()
|
||||
]
|
||||
return {"bool": {"should": metrics_conditions}}
|
||||
|
||||
def _get_next_for_current_iteration(
|
||||
self, company_id: str, navigate_earlier: bool, state: DebugImageSampleState
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Get the sample for next (if navigate_earlier is False) or previous variant sorted by name for the same iteration
|
||||
Only variants for which the iteration falls into their valid range are considered
|
||||
Return None if no such variant or sample is found
|
||||
"""
|
||||
if state.navigate_current_metric:
|
||||
variants = [
|
||||
var_state
|
||||
for var_state in state.variant_states
|
||||
if var_state.metric == state.metric
|
||||
]
|
||||
else:
|
||||
variants = state.variant_states
|
||||
|
||||
cmp = operator.lt if navigate_earlier else operator.gt
|
||||
variants = [
|
||||
var_state
|
||||
for var_state in variants
|
||||
if cmp((var_state.metric, var_state.name), (state.metric, state.variant))
|
||||
and var_state.min_iteration <= state.iteration
|
||||
]
|
||||
if not variants:
|
||||
return
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": state.task}},
|
||||
{"term": {"iter": state.iteration}},
|
||||
self._get_metric_conditions(variants),
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
order = "desc" if navigate_earlier else "asc"
|
||||
es_req = {
|
||||
"size": 1,
|
||||
"sort": [{"metric": order}, {"variant": order}],
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
}
|
||||
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=self.event_type,
|
||||
body=es_req,
|
||||
)
|
||||
|
||||
hits = nested_get(es_res, ("hits", "hits"))
|
||||
if not hits:
|
||||
return
|
||||
|
||||
return hits[0]["_source"]
|
||||
|
||||
def _get_next_for_another_iteration(
|
||||
self, company_id: str, navigate_earlier: bool, state: DebugImageSampleState
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Get the sample for the first variant for the next iteration (if navigate_earlier is set to False)
|
||||
or from the last variant for the previous iteration (otherwise)
|
||||
The variants for which the sample falls in invalid range are discarded
|
||||
If no suitable sample is found then None is returned
|
||||
"""
|
||||
if state.navigate_current_metric:
|
||||
variants = [
|
||||
var_state
|
||||
for var_state in state.variant_states
|
||||
if var_state.metric == state.metric
|
||||
]
|
||||
else:
|
||||
variants = state.variant_states
|
||||
|
||||
if navigate_earlier:
|
||||
range_operator = "lt"
|
||||
order = "desc"
|
||||
variants = [
|
||||
var_state
|
||||
for var_state in variants
|
||||
if var_state.min_iteration < state.iteration
|
||||
]
|
||||
else:
|
||||
range_operator = "gt"
|
||||
order = "asc"
|
||||
variants = variants
|
||||
|
||||
if not variants:
|
||||
return
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": state.task}},
|
||||
self._get_metric_conditions(variants),
|
||||
{"range": {"iter": {range_operator: state.iteration}}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
es_req = {
|
||||
"size": 1,
|
||||
"sort": [{"iter": order}, {"metric": order}, {"variant": order}],
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
}
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=self.event_type,
|
||||
body=es_req,
|
||||
)
|
||||
|
||||
hits = nested_get(es_res, ("hits", "hits"))
|
||||
if not hits:
|
||||
return
|
||||
|
||||
return hits[0]["_source"]
|
||||
|
||||
def get_sample_for_variant(
|
||||
self,
|
||||
company_id: str,
|
||||
task: str,
|
||||
metric: str,
|
||||
variant: str,
|
||||
iteration: Optional[int] = None,
|
||||
refresh: bool = False,
|
||||
state_id: str = None,
|
||||
navigate_current_metric: bool = True,
|
||||
) -> VariantSampleResult:
|
||||
"""
|
||||
Get the sample for the requested iteration or the latest before it
|
||||
If the iteration is not passed then get the latest event
|
||||
"""
|
||||
res = VariantSampleResult()
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=self.event_type):
|
||||
return res
|
||||
|
||||
def init_state(state_: DebugImageSampleState):
|
||||
state_.task = task
|
||||
state_.metric = metric
|
||||
state_.navigate_current_metric = navigate_current_metric
|
||||
self._reset_variant_states(company_id=company_id, state=state_)
|
||||
|
||||
def validate_state(state_: DebugImageSampleState):
|
||||
if (
|
||||
state_.task != task
|
||||
or state_.navigate_current_metric != navigate_current_metric
|
||||
or (state_.navigate_current_metric and state_.metric != metric)
|
||||
):
|
||||
raise errors.bad_request.InvalidScrollId(
|
||||
"Task and metric stored in the state do not match the passed ones",
|
||||
scroll_id=state_.id,
|
||||
)
|
||||
# fix old variant states:
|
||||
for vs in state_.variant_states:
|
||||
if vs.metric is None:
|
||||
vs.metric = metric
|
||||
if refresh:
|
||||
self._reset_variant_states(company_id=company_id, state=state_)
|
||||
|
||||
state: DebugImageSampleState
|
||||
with self.cache_manager.get_or_create_state(
|
||||
state_id=state_id, init_state=init_state, validate_state=validate_state,
|
||||
) as state:
|
||||
res.scroll_id = state.id
|
||||
|
||||
var_state = first(
|
||||
vs
|
||||
for vs in state.variant_states
|
||||
if vs.name == variant and vs.metric == metric
|
||||
)
|
||||
if not var_state:
|
||||
return res
|
||||
|
||||
res.min_iteration = var_state.min_iteration
|
||||
res.max_iteration = var_state.max_iteration
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": task}},
|
||||
{"term": {"metric": metric}},
|
||||
{"term": {"variant": variant}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
if iteration is not None:
|
||||
must_conditions.append(
|
||||
{
|
||||
"range": {
|
||||
"iter": {"lte": iteration, "gte": var_state.min_iteration}
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
must_conditions.append(
|
||||
{"range": {"iter": {"gte": var_state.min_iteration}}}
|
||||
)
|
||||
|
||||
es_req = {
|
||||
"size": 1,
|
||||
"sort": {"iter": "desc"},
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
}
|
||||
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=self.event_type,
|
||||
body=es_req,
|
||||
)
|
||||
|
||||
hits = nested_get(es_res, ("hits", "hits"))
|
||||
if not hits:
|
||||
return res
|
||||
|
||||
self._fill_res_and_update_state(
|
||||
event=hits[0]["_source"], res=res, state=state
|
||||
)
|
||||
return res
|
||||
|
||||
def _reset_variant_states(self, company_id: str, state: DebugImageSampleState):
|
||||
metrics = self._get_metric_variant_iterations(
|
||||
company_id=company_id,
|
||||
task=state.task,
|
||||
metric=state.metric if state.navigate_current_metric else None,
|
||||
)
|
||||
state.variant_states = [
|
||||
VariantState(
|
||||
metric=metric,
|
||||
name=var_name,
|
||||
min_iteration=min_iter,
|
||||
max_iteration=max_iter,
|
||||
)
|
||||
for metric, variants in metrics.items()
|
||||
for var_name, min_iter, max_iter in variants
|
||||
]
|
||||
|
||||
def _get_metric_variant_iterations(
|
||||
self, company_id: str, task: str, metric: str,
|
||||
) -> Mapping[str, Sequence[Tuple[str, int, int]]]:
|
||||
"""
|
||||
Return valid min and max iterations that the task reported events of the required type
|
||||
"""
|
||||
must = [
|
||||
{"term": {"task": task}},
|
||||
{"exists": {"field": "url"}},
|
||||
]
|
||||
if metric is not None:
|
||||
must.append({"term": {"metric": metric}})
|
||||
query = {"bool": {"must": must}}
|
||||
|
||||
search_args = dict(
|
||||
es=self.es, company_id=company_id, event_type=self.event_type,
|
||||
)
|
||||
max_metrics, max_variants = get_max_metric_and_variant_counts(
|
||||
query=query, **search_args
|
||||
)
|
||||
max_variants = int(max_variants // 2)
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": max_metrics,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"variants": {
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": max_variants,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"last_iter": {"max": {"field": "iter"}},
|
||||
"urls": {
|
||||
# group by urls and choose the minimal iteration
|
||||
# from all the maximal iterations per url
|
||||
"terms": {
|
||||
"field": "url",
|
||||
"order": {"max_iter": "asc"},
|
||||
"size": 1,
|
||||
},
|
||||
"aggs": {
|
||||
# find max iteration for each url
|
||||
"max_iter": {"max": {"field": "iter"}}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
es_res = search_company_events(body=es_req, **search_args)
|
||||
|
||||
def get_variant_data(variant_bucket: dict) -> Tuple[str, int, int]:
|
||||
variant = variant_bucket["key"]
|
||||
urls = nested_get(variant_bucket, ("urls", "buckets"))
|
||||
min_iter = int(urls[0]["max_iter"]["value"])
|
||||
max_iter = int(variant_bucket["last_iter"]["value"])
|
||||
return variant, min_iter, max_iter
|
||||
|
||||
return {
|
||||
metric_bucket["key"]: [
|
||||
get_variant_data(variant_bucket)
|
||||
for variant_bucket in nested_get(metric_bucket, ("variants", "buckets"))
|
||||
]
|
||||
for metric_bucket in nested_get(
|
||||
es_res, ("aggregations", "metrics", "buckets")
|
||||
)
|
||||
}
|
||||
316
apiserver/bll/event/history_plots_iterator.py
Normal file
316
apiserver/bll/event/history_plots_iterator.py
Normal file
@@ -0,0 +1,316 @@
|
||||
from typing import Sequence, Tuple, Optional, Mapping
|
||||
|
||||
import attr
|
||||
from boltons.iterutils import first
|
||||
from elasticsearch import Elasticsearch
|
||||
from jsonmodels.fields import StringField, IntField, ListField, BoolField
|
||||
from jsonmodels.models import Base
|
||||
from redis.client import StrictRedis
|
||||
|
||||
from .event_common import (
|
||||
EventType,
|
||||
uncompress_plot,
|
||||
EventSettings,
|
||||
check_empty_data,
|
||||
search_company_events,
|
||||
)
|
||||
from apiserver.apimodels import JsonSerializableMixin
|
||||
from apiserver.utilities.dicts import nested_get
|
||||
from apiserver.bll.redis_cache_manager import RedisCacheManager
|
||||
from apiserver.apierrors import errors
|
||||
|
||||
|
||||
class MetricState(Base):
|
||||
name: str = StringField(default=None)
|
||||
min_iteration: int = IntField()
|
||||
max_iteration: int = IntField()
|
||||
|
||||
|
||||
class PlotsSampleState(Base, JsonSerializableMixin):
|
||||
id: str = StringField(required=True)
|
||||
iteration: int = IntField()
|
||||
task: str = StringField()
|
||||
metric: str = StringField()
|
||||
metric_states: Sequence[MetricState] = ListField([MetricState])
|
||||
warning: str = StringField()
|
||||
navigate_current_metric = BoolField(default=True)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class MetricSamplesResult(object):
|
||||
scroll_id: str = None
|
||||
events: list = []
|
||||
min_iteration: int = None
|
||||
max_iteration: int = None
|
||||
|
||||
|
||||
class HistoryPlotsIterator:
|
||||
event_type = EventType.metrics_plot
|
||||
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch):
|
||||
self.es = es
|
||||
self.cache_manager = RedisCacheManager(
|
||||
state_class=PlotsSampleState,
|
||||
redis=redis,
|
||||
expiration_interval=EventSettings.state_expiration_sec,
|
||||
)
|
||||
|
||||
def get_next_sample(
|
||||
self,
|
||||
company_id: str,
|
||||
task: str,
|
||||
state_id: str,
|
||||
navigate_earlier: bool,
|
||||
next_iteration: bool,
|
||||
) -> MetricSamplesResult:
|
||||
"""
|
||||
Get the samples for next/prev metric on the current iteration
|
||||
If does not exist then try getting sample for the first/last metric from next/prev iteration
|
||||
"""
|
||||
res = MetricSamplesResult(scroll_id=state_id)
|
||||
state = self.cache_manager.get_state(state_id)
|
||||
if not state or state.task != task:
|
||||
raise errors.bad_request.InvalidScrollId(scroll_id=state_id)
|
||||
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=self.event_type):
|
||||
return res
|
||||
|
||||
if navigate_earlier:
|
||||
range_operator = "lt"
|
||||
order = "desc"
|
||||
else:
|
||||
range_operator = "gt"
|
||||
order = "asc"
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": state.task}},
|
||||
]
|
||||
if state.navigate_current_metric:
|
||||
must_conditions.append({"term": {"metric": state.metric}})
|
||||
|
||||
next_iteration_condition = {
|
||||
"range": {"iter": {range_operator: state.iteration}}
|
||||
}
|
||||
if next_iteration or state.navigate_current_metric:
|
||||
must_conditions.append(next_iteration_condition)
|
||||
else:
|
||||
next_metric_condition = {
|
||||
"bool": {
|
||||
"must": [
|
||||
{"term": {"iter": state.iteration}},
|
||||
{"range": {"metric": {range_operator: state.metric}}},
|
||||
]
|
||||
}
|
||||
}
|
||||
must_conditions.append(
|
||||
{"bool": {"should": [next_metric_condition, next_iteration_condition]}}
|
||||
)
|
||||
|
||||
events = self._get_metric_events_for_condition(
|
||||
company_id=company_id,
|
||||
task=state.task,
|
||||
order=order,
|
||||
must_conditions=must_conditions,
|
||||
)
|
||||
|
||||
if not events:
|
||||
return res
|
||||
|
||||
self._fill_res_and_update_state(events=events, res=res, state=state)
|
||||
self.cache_manager.set_state(state=state)
|
||||
return res
|
||||
|
||||
def get_samples_for_metric(
|
||||
self,
|
||||
company_id: str,
|
||||
task: str,
|
||||
metric: str,
|
||||
iteration: Optional[int] = None,
|
||||
refresh: bool = False,
|
||||
state_id: str = None,
|
||||
navigate_current_metric: bool = True,
|
||||
) -> MetricSamplesResult:
|
||||
"""
|
||||
Get the sample for the requested iteration or the latest before it
|
||||
If the iteration is not passed then get the latest event
|
||||
"""
|
||||
res = MetricSamplesResult()
|
||||
if check_empty_data(self.es, company_id=company_id, event_type=self.event_type):
|
||||
return res
|
||||
|
||||
def init_state(state_: PlotsSampleState):
|
||||
state_.task = task
|
||||
state_.metric = metric
|
||||
state_.navigate_current_metric = navigate_current_metric
|
||||
self._reset_metric_states(company_id=company_id, state=state_)
|
||||
|
||||
def validate_state(state_: PlotsSampleState):
|
||||
if (
|
||||
state_.task != task
|
||||
or state_.navigate_current_metric != navigate_current_metric
|
||||
or (state_.navigate_current_metric and state_.metric != metric)
|
||||
):
|
||||
raise errors.bad_request.InvalidScrollId(
|
||||
"Task and metric stored in the state do not match the passed ones",
|
||||
scroll_id=state_.id,
|
||||
)
|
||||
if refresh:
|
||||
self._reset_metric_states(company_id=company_id, state=state_)
|
||||
|
||||
state: PlotsSampleState
|
||||
with self.cache_manager.get_or_create_state(
|
||||
state_id=state_id, init_state=init_state, validate_state=validate_state,
|
||||
) as state:
|
||||
res.scroll_id = state.id
|
||||
|
||||
metric_state = first(ms for ms in state.metric_states if ms.name == metric)
|
||||
if not metric_state:
|
||||
return res
|
||||
|
||||
res.min_iteration = metric_state.min_iteration
|
||||
res.max_iteration = metric_state.max_iteration
|
||||
|
||||
must_conditions = [
|
||||
{"term": {"task": task}},
|
||||
{"term": {"metric": metric}},
|
||||
]
|
||||
if iteration is not None:
|
||||
must_conditions.append({"range": {"iter": {"lte": iteration}}})
|
||||
|
||||
events = self._get_metric_events_for_condition(
|
||||
company_id=company_id,
|
||||
task=state.task,
|
||||
order="desc",
|
||||
must_conditions=must_conditions,
|
||||
)
|
||||
if not events:
|
||||
return res
|
||||
|
||||
self._fill_res_and_update_state(events=events, res=res, state=state)
|
||||
return res
|
||||
|
||||
def _reset_metric_states(self, company_id: str, state: PlotsSampleState):
|
||||
metrics = self._get_metric_iterations(
|
||||
company_id=company_id,
|
||||
task=state.task,
|
||||
metric=state.metric if state.navigate_current_metric else None,
|
||||
)
|
||||
state.metric_states = [
|
||||
MetricState(name=metric, min_iteration=min_iter, max_iteration=max_iter)
|
||||
for metric, (min_iter, max_iter) in metrics.items()
|
||||
]
|
||||
|
||||
def _get_metric_iterations(
|
||||
self, company_id: str, task: str, metric: str,
|
||||
) -> Mapping[str, Tuple[int, int]]:
|
||||
"""
|
||||
Return valid min and max iterations that the task reported events of the required type
|
||||
"""
|
||||
must = [
|
||||
{"term": {"task": task}},
|
||||
]
|
||||
if metric is not None:
|
||||
must.append({"term": {"metric": metric}})
|
||||
query = {"bool": {"must": must}}
|
||||
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": 5000,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"last_iter": {"max": {"field": "iter"}},
|
||||
"first_iter": {"min": {"field": "iter"}},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
es_res = search_company_events(
|
||||
body=es_req,
|
||||
es=self.es,
|
||||
company_id=company_id,
|
||||
event_type=self.event_type,
|
||||
)
|
||||
|
||||
return {
|
||||
metric_bucket["key"]: (
|
||||
int(metric_bucket["first_iter"]["value"]),
|
||||
int(metric_bucket["last_iter"]["value"]),
|
||||
)
|
||||
for metric_bucket in nested_get(
|
||||
es_res, ("aggregations", "metrics", "buckets")
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _fill_res_and_update_state(
|
||||
events: Sequence[dict], res: MetricSamplesResult, state: PlotsSampleState
|
||||
):
|
||||
for event in events:
|
||||
uncompress_plot(event)
|
||||
state.metric = events[0]["metric"]
|
||||
state.iteration = events[0]["iter"]
|
||||
res.events = events
|
||||
metric_state = first(
|
||||
ms for ms in state.metric_states if ms.name == state.metric
|
||||
)
|
||||
if metric_state:
|
||||
res.min_iteration = metric_state.min_iteration
|
||||
res.max_iteration = metric_state.max_iteration
|
||||
|
||||
def _get_metric_events_for_condition(
|
||||
self, company_id: str, task: str, order: str, must_conditions: Sequence
|
||||
) -> Sequence:
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
"aggs": {
|
||||
"iters": {
|
||||
"terms": {"field": "iter", "size": 1, "order": {"_key": order}},
|
||||
"aggs": {
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": 1,
|
||||
"order": {"_key": order},
|
||||
},
|
||||
"aggs": {
|
||||
"events": {
|
||||
"top_hits": {
|
||||
"sort": {"variant": {"order": "asc"}},
|
||||
"size": 100,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
es_res = search_company_events(
|
||||
self.es,
|
||||
company_id=company_id,
|
||||
event_type=self.event_type,
|
||||
body=es_req,
|
||||
)
|
||||
|
||||
aggs_result = es_res.get("aggregations")
|
||||
if not aggs_result:
|
||||
return []
|
||||
|
||||
for level in ("iters", "metrics"):
|
||||
level_data = aggs_result[level]["buckets"]
|
||||
if not level_data:
|
||||
return []
|
||||
aggs_result = level_data[0]
|
||||
|
||||
return [
|
||||
hit["_source"]
|
||||
for hit in nested_get(aggs_result, ("events", "hits", "hits"))
|
||||
]
|
||||
53
apiserver/bll/event/metric_debug_images_iterator.py
Normal file
53
apiserver/bll/event/metric_debug_images_iterator.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import Sequence, Tuple, Callable
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from redis.client import StrictRedis
|
||||
|
||||
from apiserver.utilities.dicts import nested_get
|
||||
from .event_common import EventType
|
||||
from .metric_events_iterator import MetricEventsIterator, VariantState
|
||||
|
||||
|
||||
class MetricDebugImagesIterator(MetricEventsIterator):
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch):
|
||||
super().__init__(redis, es, EventType.metrics_image)
|
||||
|
||||
def _get_extra_conditions(self) -> Sequence[dict]:
|
||||
return [{"exists": {"field": "url"}}]
|
||||
|
||||
def _get_variant_state_aggs(self) -> Tuple[dict, Callable[[dict, VariantState], None]]:
|
||||
aggs = {
|
||||
"urls": {
|
||||
"terms": {
|
||||
"field": "url",
|
||||
"order": {"max_iter": "desc"},
|
||||
"size": 1, # we need only one url from the most recent iteration
|
||||
},
|
||||
"aggs": {
|
||||
"max_iter": {"max": {"field": "iter"}},
|
||||
"iters": {
|
||||
"top_hits": {
|
||||
"sort": {"iter": {"order": "desc"}},
|
||||
"size": 2, # need two last iterations so that we can take
|
||||
# the second one as invalid
|
||||
"_source": "iter",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
def fill_variant_state_data(variant_bucket: dict, state: VariantState):
|
||||
"""If the image urls get recycled then fill the last_invalid_iteration field"""
|
||||
top_iter_url = nested_get(variant_bucket, ("urls", "buckets"))[0]
|
||||
iters = nested_get(top_iter_url, ("iters", "hits", "hits"))
|
||||
if len(iters) > 1:
|
||||
state.last_invalid_iteration = nested_get(iters[1], ("_source", "iter"))
|
||||
|
||||
return aggs, fill_variant_state_data
|
||||
|
||||
def _process_event(self, event: dict) -> dict:
|
||||
return event
|
||||
|
||||
def _get_same_variant_events_order(self) -> dict:
|
||||
return {"url": {"order": "desc"}}
|
||||
@@ -1,8 +1,9 @@
|
||||
import abc
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from operator import itemgetter
|
||||
from typing import Sequence, Tuple, Optional, Mapping
|
||||
from typing import Sequence, Tuple, Optional, Mapping, Callable
|
||||
|
||||
import attr
|
||||
import dpath
|
||||
@@ -19,12 +20,13 @@ from apiserver.bll.event.event_common import (
|
||||
search_company_events,
|
||||
EventType,
|
||||
get_metric_variants_condition,
|
||||
get_max_metric_and_variant_counts,
|
||||
)
|
||||
from apiserver.bll.redis_cache_manager import RedisCacheManager
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.task.metrics import MetricEventStats
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.timing_context import TimingContext
|
||||
|
||||
|
||||
class VariantState(Base):
|
||||
@@ -49,65 +51,78 @@ class TaskScrollState(Base):
|
||||
self.last_min_iter = self.last_max_iter = None
|
||||
|
||||
|
||||
class DebugImageEventsScrollState(Base, JsonSerializableMixin):
|
||||
class MetricEventsScrollState(Base, JsonSerializableMixin):
|
||||
id: str = StringField(required=True)
|
||||
tasks: Sequence[TaskScrollState] = ListField([TaskScrollState])
|
||||
warning: str = StringField()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
class DebugImagesResult(object):
|
||||
class MetricEventsResult(object):
|
||||
metric_events: Sequence[tuple] = []
|
||||
next_scroll_id: str = None
|
||||
|
||||
|
||||
class DebugImagesIterator:
|
||||
EVENT_TYPE = EventType.metrics_image
|
||||
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch):
|
||||
class MetricEventsIterator:
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch, event_type: EventType):
|
||||
self.es = es
|
||||
self.event_type = event_type
|
||||
self.cache_manager = RedisCacheManager(
|
||||
state_class=DebugImageEventsScrollState,
|
||||
state_class=MetricEventsScrollState,
|
||||
redis=redis,
|
||||
expiration_interval=EventSettings.state_expiration_sec,
|
||||
)
|
||||
|
||||
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,
|
||||
) -> DebugImagesResult:
|
||||
if check_empty_data(self.es, company_id, self.EVENT_TYPE):
|
||||
return DebugImagesResult()
|
||||
) -> MetricEventsResult:
|
||||
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_: DebugImageEventsScrollState):
|
||||
state_.tasks = self._init_task_states(company_id, task_metrics)
|
||||
def init_state(state_: MetricEventsScrollState):
|
||||
state_.tasks = self._init_task_states(companies, task_metrics)
|
||||
|
||||
def validate_state(state_: DebugImageEventsScrollState):
|
||||
def validate_state(state_: MetricEventsScrollState):
|
||||
"""
|
||||
Validate that the metrics stored in the state are the same
|
||||
as requested in the current call.
|
||||
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
|
||||
) as state:
|
||||
res = DebugImagesResult(next_scroll_id=state.id)
|
||||
res = MetricEventsResult(next_scroll_id=state.id)
|
||||
specific_variants_requested = any(
|
||||
variants
|
||||
for t, metrics in task_metrics.items()
|
||||
if metrics
|
||||
for m, variants in metrics.items()
|
||||
)
|
||||
with ThreadPoolExecutor(EventSettings.max_workers) as pool:
|
||||
res.metric_events = list(
|
||||
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,
|
||||
),
|
||||
state.tasks,
|
||||
)
|
||||
@@ -117,22 +132,20 @@ class DebugImagesIterator:
|
||||
|
||||
def _reinit_outdated_task_states(
|
||||
self,
|
||||
company_id,
|
||||
state: DebugImageEventsScrollState,
|
||||
companies: Mapping[str, str],
|
||||
state: MetricEventsScrollState,
|
||||
task_metrics: Mapping[str, dict],
|
||||
):
|
||||
"""
|
||||
Determine the metrics for which new debug image events were added
|
||||
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,
|
||||
) -> Mapping[str, datetime]:
|
||||
"""For metrics that reported debug image events get mapping of the metric name to the last update times"""
|
||||
"""For metrics that reported event_type events get mapping of the metric name to the last update times"""
|
||||
metric_stats: Mapping[str, MetricEventStats] = task.metric_stats
|
||||
if not metric_stats:
|
||||
return {}
|
||||
@@ -140,10 +153,10 @@ class DebugImagesIterator:
|
||||
requested_metrics = task_metrics[task.id]
|
||||
return {
|
||||
stats.metric: stats.event_stats_by_type[
|
||||
self.EVENT_TYPE.value
|
||||
self.event_type.value
|
||||
].last_update
|
||||
for stats in metric_stats.values()
|
||||
if self.EVENT_TYPE.value in stats.event_stats_by_type
|
||||
if self.event_type.value in stats.event_stats_by_type
|
||||
and (not requested_metrics or stats.metric in requested_metrics)
|
||||
}
|
||||
|
||||
@@ -167,7 +180,7 @@ class DebugImagesIterator:
|
||||
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]
|
||||
@@ -197,14 +210,14 @@ class DebugImagesIterator:
|
||||
]
|
||||
|
||||
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(),
|
||||
)
|
||||
|
||||
@@ -213,18 +226,38 @@ class DebugImagesIterator:
|
||||
for task, metric_states in zip(task_metrics, task_metric_states)
|
||||
]
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_extra_conditions(self) -> Sequence[dict]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_variant_state_aggs(
|
||||
self,
|
||||
) -> Tuple[dict, Callable[[dict, VariantState], None]]:
|
||||
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 debug images
|
||||
for the variants that reported any event_type events
|
||||
"""
|
||||
task, metrics = task_metrics
|
||||
must = [{"term": {"task": task}}, {"exists": {"field": "url"}}]
|
||||
company_id = companies[task]
|
||||
must = [{"term": {"task": task}}, *self._get_extra_conditions()]
|
||||
if metrics:
|
||||
must.append(get_metric_variants_condition(metrics))
|
||||
query = {"bool": {"must": must}}
|
||||
|
||||
search_args = dict(
|
||||
es=self.es, company_id=company_id, event_type=self.event_type
|
||||
)
|
||||
max_metrics, max_variants = get_max_metric_and_variant_counts(
|
||||
query=query, **search_args
|
||||
)
|
||||
max_variants = int(max_variants // 2)
|
||||
variant_state_aggs, fill_variant_state_data = self._get_variant_state_aggs()
|
||||
es_req: dict = {
|
||||
"size": 0,
|
||||
"query": query,
|
||||
@@ -232,7 +265,7 @@ class DebugImagesIterator:
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": EventSettings.max_metrics_count,
|
||||
"size": max_metrics,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
@@ -240,52 +273,33 @@ class DebugImagesIterator:
|
||||
"variants": {
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": EventSettings.max_variants_count,
|
||||
"size": max_variants,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"urls": {
|
||||
"terms": {
|
||||
"field": "url",
|
||||
"order": {"max_iter": "desc"},
|
||||
"size": 1, # we need only one url from the most recent iteration
|
||||
},
|
||||
"aggs": {
|
||||
"max_iter": {"max": {"field": "iter"}},
|
||||
"iters": {
|
||||
"top_hits": {
|
||||
"sort": {"iter": {"order": "desc"}},
|
||||
"size": 2, # need two last iterations so that we can take
|
||||
# the second one as invalid
|
||||
"_source": "iter",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
**(
|
||||
{"aggs": variant_state_aggs}
|
||||
if variant_state_aggs
|
||||
else {}
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "_init_metric_states"):
|
||||
es_res = search_company_events(
|
||||
self.es, company_id=company_id, event_type=self.EVENT_TYPE, body=es_req,
|
||||
)
|
||||
with translate_errors_context():
|
||||
es_res = search_company_events(body=es_req, **search_args)
|
||||
if "aggregations" not in es_res:
|
||||
return []
|
||||
|
||||
def init_variant_state(variant: dict):
|
||||
"""
|
||||
Return new variant state for the passed variant bucket
|
||||
If the image urls get recycled then fill the last_invalid_iteration field
|
||||
"""
|
||||
state = VariantState(variant=variant["key"])
|
||||
top_iter_url = dpath.get(variant, "urls/buckets")[0]
|
||||
iters = dpath.get(top_iter_url, "iters/hits/hits")
|
||||
if len(iters) > 1:
|
||||
state.last_invalid_iteration = dpath.get(iters[1], "_source/iter")
|
||||
if fill_variant_state_data:
|
||||
fill_variant_state_data(variant, state)
|
||||
|
||||
return state
|
||||
|
||||
return [
|
||||
@@ -300,12 +314,21 @@ class DebugImagesIterator:
|
||||
for metric in dpath.get(es_res, "aggregations/metrics/buckets")
|
||||
]
|
||||
|
||||
@abc.abstractmethod
|
||||
def _process_event(self, event: dict) -> dict:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_same_variant_events_order(self) -> dict:
|
||||
pass
|
||||
|
||||
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,
|
||||
) -> Tuple:
|
||||
"""
|
||||
Return task metric events grouped by iterations
|
||||
@@ -321,7 +344,7 @@ class DebugImagesIterator:
|
||||
must_conditions = [
|
||||
{"term": {"task": task_state.task}},
|
||||
{"terms": {"metric": [m.metric for m in task_state.metrics]}},
|
||||
{"exists": {"field": "url"}},
|
||||
*self._get_extra_conditions(),
|
||||
]
|
||||
|
||||
range_condition = None
|
||||
@@ -332,6 +355,8 @@ class DebugImagesIterator:
|
||||
if range_condition:
|
||||
must_conditions.append({"range": {"iter": range_condition}})
|
||||
|
||||
metrics_count = len(task_state.metrics)
|
||||
max_variants = int(EventSettings.max_es_buckets / (metrics_count * iter_count))
|
||||
es_req = {
|
||||
"size": 0,
|
||||
"query": {"bool": {"must": must_conditions}},
|
||||
@@ -346,20 +371,20 @@ class DebugImagesIterator:
|
||||
"metrics": {
|
||||
"terms": {
|
||||
"field": "metric",
|
||||
"size": EventSettings.max_metrics_count,
|
||||
"size": metrics_count,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"variants": {
|
||||
"terms": {
|
||||
"field": "variant",
|
||||
"size": EventSettings.max_variants_count,
|
||||
"size": max_variants,
|
||||
"order": {"_key": "asc"},
|
||||
},
|
||||
"aggs": {
|
||||
"events": {
|
||||
"top_hits": {
|
||||
"sort": {"url": {"order": "desc"}}
|
||||
"sort": self._get_same_variant_events_order()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -370,9 +395,12 @@ class DebugImagesIterator:
|
||||
}
|
||||
},
|
||||
}
|
||||
with translate_errors_context(), TimingContext("es", "get_debug_image_events"):
|
||||
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, []
|
||||
@@ -382,18 +410,26 @@ class DebugImagesIterator:
|
||||
for m in task_state.metrics
|
||||
for v in m.variants
|
||||
}
|
||||
allow_uninitialized = (
|
||||
False
|
||||
if specific_variants_requested
|
||||
else config.get(
|
||||
"services.events.events_retrieval.debug_images.allow_uninitialized_variants",
|
||||
False,
|
||||
)
|
||||
)
|
||||
|
||||
def is_valid_event(event: dict) -> bool:
|
||||
key = event.get("metric"), event.get("variant")
|
||||
if key not in invalid_iterations:
|
||||
return False
|
||||
return allow_uninitialized
|
||||
|
||||
max_invalid = invalid_iterations[key]
|
||||
return max_invalid is None or event.get("iter") > max_invalid
|
||||
|
||||
def get_iteration_events(it_: dict) -> Sequence:
|
||||
return [
|
||||
ev["_source"]
|
||||
self._process_event(ev["_source"])
|
||||
for m in dpath.get(it_, "metrics/buckets")
|
||||
for v in dpath.get(m, "variants/buckets")
|
||||
for ev in dpath.get(v, "events/hits/hits")
|
||||
25
apiserver/bll/event/metric_plots_iterator.py
Normal file
25
apiserver/bll/event/metric_plots_iterator.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from typing import Sequence
|
||||
|
||||
from elasticsearch import Elasticsearch
|
||||
from redis.client import StrictRedis
|
||||
|
||||
from .event_common import EventType, uncompress_plot
|
||||
from .metric_events_iterator import MetricEventsIterator
|
||||
|
||||
|
||||
class MetricPlotsIterator(MetricEventsIterator):
|
||||
def __init__(self, redis: StrictRedis, es: Elasticsearch):
|
||||
super().__init__(redis, es, EventType.metrics_plot)
|
||||
|
||||
def _get_extra_conditions(self) -> Sequence[dict]:
|
||||
return []
|
||||
|
||||
def _get_variant_state_aggs(self):
|
||||
return None, None
|
||||
|
||||
def _process_event(self, event: dict) -> dict:
|
||||
uncompress_plot(event)
|
||||
return event
|
||||
|
||||
def _get_same_variant_events_order(self) -> dict:
|
||||
return {"timestamp": {"order": "desc"}}
|
||||
@@ -1,9 +1,12 @@
|
||||
from datetime import datetime
|
||||
from typing import Callable, Tuple
|
||||
from typing import Callable, Tuple, Sequence, Dict, Optional
|
||||
|
||||
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
|
||||
@@ -11,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
|
||||
@@ -24,13 +29,37 @@ class ModelBLL:
|
||||
raise errors.bad_request.InvalidModelId(**query)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def assert_exists(
|
||||
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)
|
||||
query = Q(id__in=ids)
|
||||
|
||||
q = Model.get_many(
|
||||
company=company_id,
|
||||
query=query,
|
||||
allow_public=allow_public,
|
||||
return_dicts=False,
|
||||
)
|
||||
if only:
|
||||
q = q.only(*only)
|
||||
|
||||
if q.count() != len(ids):
|
||||
raise errors.bad_request.InvalidModelId(ids=model_ids)
|
||||
|
||||
if return_models:
|
||||
return list(q)
|
||||
|
||||
@classmethod
|
||||
def publish_model(
|
||||
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:
|
||||
@@ -45,7 +74,7 @@ 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
|
||||
@@ -56,7 +85,7 @@ class ModelBLL:
|
||||
|
||||
@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,
|
||||
@@ -82,26 +111,58 @@ 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_change": now,
|
||||
},
|
||||
array_filters=[{"elem.model": model_id}],
|
||||
upsert=False,
|
||||
)
|
||||
else:
|
||||
task.update(
|
||||
pull__models__output__model=model_id, set__last_change=now
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -128,3 +189,57 @@ class ModelBLL:
|
||||
)
|
||||
|
||||
return unarchived
|
||||
|
||||
@classmethod
|
||||
def get_model_stats(
|
||||
cls, company: str, model_ids: Sequence[str],
|
||||
) -> Dict[str, dict]:
|
||||
if not model_ids:
|
||||
return {}
|
||||
|
||||
result = Model.aggregate(
|
||||
[
|
||||
{
|
||||
"$match": {
|
||||
"company": {"$in": [None, "", company]},
|
||||
"_id": {"$in": model_ids},
|
||||
}
|
||||
},
|
||||
{
|
||||
"$addFields": {
|
||||
"labels_count": {"$size": {"$objectToArray": "$labels"}}
|
||||
}
|
||||
},
|
||||
{"$project": {"labels_count": 1}},
|
||||
]
|
||||
)
|
||||
return {r.pop("_id"): r for r in result}
|
||||
|
||||
@staticmethod
|
||||
def update_statistics(
|
||||
company_id: str,
|
||||
model_id: str,
|
||||
last_iteration_max: int = None,
|
||||
last_scalar_events: Dict[str, Dict[str, dict]] = None,
|
||||
):
|
||||
updates = {"last_update": datetime.utcnow()}
|
||||
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,13 +5,11 @@ 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,
|
||||
)
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.timing_context import TimingContext
|
||||
|
||||
log = config.logger(__file__)
|
||||
|
||||
@@ -42,27 +40,25 @@ class Metadata:
|
||||
replace_metadata: bool,
|
||||
**more_updates,
|
||||
) -> int:
|
||||
with TimingContext("mongo", "edit_metadata"):
|
||||
update_cmds = dict()
|
||||
metadata = cls.metadata_from_api(items)
|
||||
if replace_metadata:
|
||||
update_cmds["set__metadata"] = metadata
|
||||
else:
|
||||
for key, value in metadata.items():
|
||||
update_cmds[f"set__metadata__{mongoengine_safe(key)}"] = value
|
||||
update_cmds = dict()
|
||||
metadata = cls.metadata_from_api(items)
|
||||
if replace_metadata:
|
||||
update_cmds["set__metadata"] = metadata
|
||||
else:
|
||||
for key, value in metadata.items():
|
||||
update_cmds[f"set__metadata__{mongoengine_safe(key)}"] = value
|
||||
|
||||
return obj.update(**update_cmds, **more_updates)
|
||||
return obj.update(**update_cmds, **more_updates)
|
||||
|
||||
@classmethod
|
||||
def delete_metadata(cls, obj: Document, keys: Sequence[str], **more_updates) -> int:
|
||||
with TimingContext("mongo", "delete_metadata"):
|
||||
return obj.update(
|
||||
**{
|
||||
f"unset__metadata__{ParameterKeyEscaper.escape(key)}": 1
|
||||
for key in set(keys)
|
||||
},
|
||||
**more_updates,
|
||||
)
|
||||
return obj.update(
|
||||
**{
|
||||
f"unset__metadata__{ParameterKeyEscaper.escape(key)}": 1
|
||||
for key in set(keys)
|
||||
},
|
||||
**more_updates,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _process_path(path: str):
|
||||
@@ -90,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))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from redis import Redis
|
||||
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.bll.project import project_ids_with_children
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.base import GetMixin
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.task.task import Task
|
||||
@@ -42,6 +43,8 @@ class _TagsCache:
|
||||
query &= GetMixin.get_list_field_query(name, vals)
|
||||
if project:
|
||||
query &= Q(project__in=project_ids_with_children([project]))
|
||||
else:
|
||||
query &= Q(system_tags__nin=[EntityVisibility.hidden.value])
|
||||
|
||||
return self.db_cls.objects(query).distinct(field)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -8,17 +11,26 @@ from apiserver.bll.task.task_cleanup import (
|
||||
collect_debug_image_urls,
|
||||
collect_plot_image_urls,
|
||||
TaskUrls,
|
||||
_schedule_for_delete,
|
||||
)
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.task.task import Task, ArtifactModes
|
||||
from apiserver.timing_context import TimingContext
|
||||
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)
|
||||
@@ -30,62 +42,165 @@ 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")
|
||||
company=company, id=project_id, _only=("id", "path", "system_tags")
|
||||
)
|
||||
if not project:
|
||||
raise errors.bad_request.InvalidProjectId(id=project_id)
|
||||
|
||||
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):
|
||||
ret[f"non_archived_{cls.__name__.lower()}s"] = cls.objects(
|
||||
project__in=project_ids,
|
||||
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
|
||||
|
||||
|
||||
def delete_project(
|
||||
company: str, project_id: str, force: bool, delete_contents: bool
|
||||
company: str,
|
||||
user: str,
|
||||
project_id: str,
|
||||
force: bool,
|
||||
delete_contents: bool,
|
||||
delete_external_artifacts: bool,
|
||||
) -> Tuple[DeleteProjectResult, Set[str]]:
|
||||
project = Project.get_for_writing(
|
||||
company=company, id=project_id, _only=("id", "path")
|
||||
company=company, id=project_id, _only=("id", "path", "system_tags")
|
||||
)
|
||||
if not project:
|
||||
raise errors.bad_request.InvalidProjectId(id=project_id)
|
||||
|
||||
project_ids = _ids_with_children([project_id])
|
||||
delete_external_artifacts = delete_external_artifacts and config.get(
|
||||
"services.async_urls_delete.enabled", True
|
||||
)
|
||||
project_ids, pipeline_ids, dataset_ids = _get_child_project_ids(project_id)
|
||||
if not force:
|
||||
for cls, error in (
|
||||
(Task, errors.bad_request.ProjectHasTasks),
|
||||
(Model, errors.bad_request.ProjectHasModels),
|
||||
):
|
||||
non_archived = cls.objects(
|
||||
project__in=project_ids,
|
||||
if pipeline_ids:
|
||||
active_controllers = Task.objects(
|
||||
project__in=pipeline_ids,
|
||||
type=TaskType.controller,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).only("id")
|
||||
if non_archived:
|
||||
raise error("use force=true to delete", id=project_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(
|
||||
project__in=regular_projects,
|
||||
system_tags__nin=[EntityVisibility.archived.value],
|
||||
).only("id")
|
||||
if non_archived:
|
||||
raise error("use force=true", id=project_id)
|
||||
|
||||
if not delete_contents:
|
||||
with TimingContext("mongo", "update_children"):
|
||||
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_urls = _delete_models(projects=project_ids)
|
||||
deleted_tasks, event_urls, artifact_urls = _delete_tasks(
|
||||
deleted_models, model_event_urls, model_urls = _delete_models(
|
||||
company=company, projects=project_ids
|
||||
)
|
||||
deleted_tasks, task_event_urls, artifact_urls = _delete_tasks(
|
||||
company=company, projects=project_ids
|
||||
)
|
||||
event_urls = task_event_urls | model_event_urls
|
||||
if delete_external_artifacts:
|
||||
scheduled = _schedule_for_delete(
|
||||
task_id=project_id,
|
||||
company=company,
|
||||
user=user,
|
||||
urls=event_urls | model_urls | artifact_urls,
|
||||
can_delete_folders=True,
|
||||
)
|
||||
for urls in (event_urls, model_urls, artifact_urls):
|
||||
urls.difference_update(scheduled)
|
||||
res = DeleteProjectResult(
|
||||
deleted_tasks=deleted_tasks,
|
||||
deleted_models=deleted_models,
|
||||
@@ -114,9 +229,8 @@ def _delete_tasks(company: str, projects: Sequence[str]) -> Tuple[int, Set, Set]
|
||||
return 0, set(), set()
|
||||
|
||||
task_ids = {t.id for t in tasks}
|
||||
with TimingContext("mongo", "delete_tasks_update_children"):
|
||||
Task.objects(parent__in=task_ids, project__nin=projects).update(parent=None)
|
||||
Model.objects(task__in=task_ids, project__nin=projects).update(task=None)
|
||||
Task.objects(parent__in=task_ids, project__nin=projects).update(parent=None)
|
||||
Model.objects(task__in=task_ids, project__nin=projects).update(task=None)
|
||||
|
||||
event_urls, artifact_urls = set(), set()
|
||||
for task in tasks:
|
||||
@@ -131,46 +245,67 @@ def _delete_tasks(company: str, projects: Sequence[str]) -> Tuple[int, Set, Set]
|
||||
}
|
||||
)
|
||||
|
||||
event_bll.delete_multi_task_events(company, list(task_ids))
|
||||
event_bll.delete_multi_task_events(
|
||||
company, list(task_ids), async_delete=async_events_delete
|
||||
)
|
||||
deleted = tasks.delete()
|
||||
return deleted, event_urls, artifact_urls
|
||||
|
||||
|
||||
def _delete_models(projects: Sequence[str]) -> Tuple[int, Set[str]]:
|
||||
def _delete_models(
|
||||
company: str, projects: Sequence[str]
|
||||
) -> Tuple[int, Set[str], Set[str]]:
|
||||
"""
|
||||
Delete project models and update the tasks from other projects
|
||||
that reference them to reference None.
|
||||
"""
|
||||
with TimingContext("mongo", "delete_models"):
|
||||
models = Model.objects(project__in=projects).only("task", "id", "uri")
|
||||
if not models:
|
||||
return 0, set()
|
||||
models = Model.objects(project__in=projects).only("task", "id", "uri")
|
||||
if not models:
|
||||
return 0, set(), set()
|
||||
|
||||
model_ids = list({m.id for m in models})
|
||||
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": 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.input.model": {"$in": model_ids},
|
||||
"models.output.model": {"$in": model_ids},
|
||||
"status": TaskStatus.published,
|
||||
},
|
||||
update={
|
||||
"$set": {"models.output.$[elem].model": deleted, "last_change": now,}
|
||||
},
|
||||
update={"$set": {"models.input.$[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)
|
||||
|
||||
model_tasks = list({m.task for m in models if m.task})
|
||||
if model_tasks:
|
||||
Task._get_collection().update_many(
|
||||
filter={
|
||||
"_id": {"$in": model_tasks},
|
||||
"project": {"$nin": projects},
|
||||
"models.output.model": {"$in": model_ids},
|
||||
},
|
||||
update={"$set": {"models.output.$[elem].model": None}},
|
||||
array_filters=[{"elem.model": {"$in": model_ids}}],
|
||||
upsert=False,
|
||||
)
|
||||
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)
|
||||
|
||||
urls = {m.uri for m in models if m.uri}
|
||||
deleted = models.delete()
|
||||
return deleted, urls
|
||||
event_bll.delete_multi_task_events(
|
||||
company, model_ids, async_delete=async_events_delete
|
||||
)
|
||||
deleted = models.delete()
|
||||
return deleted, event_urls, model_urls
|
||||
|
||||
@@ -140,7 +140,11 @@ class ProjectQueries:
|
||||
name: 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
|
||||
@@ -160,7 +164,7 @@ 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 = f"hyperparam_values_{company_id}_{'_'.join(project_ids)}_{section}_{name}_{allow_public}_{page}_{page_size}"
|
||||
last_update = last_updated_task.last_update or datetime.utcnow()
|
||||
cached_res = self._get_cached_param_values(
|
||||
key=redis_key,
|
||||
@@ -172,7 +176,6 @@ class ProjectQueries:
|
||||
if cached_res:
|
||||
return cached_res
|
||||
|
||||
max_values = config.get("services.tasks.hyperparam_values.max_count", 100)
|
||||
pipeline = [
|
||||
{
|
||||
"$match": {
|
||||
@@ -184,7 +187,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,
|
||||
@@ -209,7 +213,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 +254,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 +315,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 +339,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 +347,6 @@ class ProjectQueries:
|
||||
if cached_res:
|
||||
return cached_res
|
||||
|
||||
max_values = config.get("services.models.metadata_values.max_count", 100)
|
||||
pipeline = [
|
||||
{
|
||||
"$match": {
|
||||
@@ -346,7 +358,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,
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Tuple, Optional, Sequence, Mapping
|
||||
|
||||
from apiserver import database
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.database.model import EntityVisibility
|
||||
from apiserver.database.model.project import Project
|
||||
|
||||
name_separator = "/"
|
||||
@@ -13,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])
|
||||
|
||||
@@ -33,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
|
||||
|
||||
@@ -42,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,
|
||||
@@ -50,6 +52,7 @@ def _ensure_project(
|
||||
created=now,
|
||||
last_update=now,
|
||||
name=name,
|
||||
basename=name.split("/")[-1],
|
||||
**(creation_params or dict(description="")),
|
||||
)
|
||||
parent = _ensure_project(company, user, location, creation_params=creation_params)
|
||||
@@ -99,13 +102,25 @@ 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")
|
||||
) -> Mapping[str, Sequence[Project]]:
|
||||
project_ids: Sequence[str],
|
||||
_only: Sequence[str] = ("id", "path"),
|
||||
search_hidden=True,
|
||||
allowed_ids: Sequence[str] = None,
|
||||
) -> ProjectsChildren:
|
||||
"""
|
||||
Return the list of child projects of all the levels for the parent project ids
|
||||
"""
|
||||
qs = Project.objects(path__in=project_ids)
|
||||
query = dict(path__in=project_ids)
|
||||
if not search_hidden:
|
||||
query["system_tags__nin"] = [EntityVisibility.hidden.value]
|
||||
if allowed_ids:
|
||||
query["id__in"] = allowed_ids
|
||||
|
||||
qs = Project.objects(**query)
|
||||
if _only:
|
||||
_only = set(_only) | {"path"}
|
||||
qs = qs.only(*_only)
|
||||
@@ -129,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(
|
||||
@@ -145,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
|
||||
@@ -166,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,10 +1,12 @@
|
||||
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
|
||||
|
||||
from apiserver import database
|
||||
from apiserver.database.model.task.task import Task, TaskStatus
|
||||
from apiserver.es_factory import es_factory
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.bll.queue.queue_metrics import QueueMetrics
|
||||
@@ -14,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):
|
||||
@@ -50,8 +54,25 @@ class QueueBLL(object):
|
||||
queue.save()
|
||||
return queue
|
||||
|
||||
def get_by_name(
|
||||
self, company_id: str, queue_name: str, only: Optional[Sequence[str]] = None,
|
||||
) -> Queue:
|
||||
qs = Queue.objects(name=queue_name, company=company_id)
|
||||
if only:
|
||||
qs = qs.only(*only)
|
||||
|
||||
return qs.first()
|
||||
|
||||
@staticmethod
|
||||
def _get_task_entries_projection(max_task_entries: int) -> dict:
|
||||
return dict(slice__entries=max_task_entries)
|
||||
|
||||
def get_by_id(
|
||||
self, company_id: str, queue_id: str, only: Optional[Sequence[str]] = None
|
||||
self,
|
||||
company_id: str,
|
||||
queue_id: str,
|
||||
only: Optional[Sequence[str]] = None,
|
||||
max_task_entries: int = None,
|
||||
) -> Queue:
|
||||
"""
|
||||
Get queue by id
|
||||
@@ -62,6 +83,8 @@ class QueueBLL(object):
|
||||
qs = Queue.objects(**query)
|
||||
if only:
|
||||
qs = qs.only(*only)
|
||||
if max_task_entries:
|
||||
qs = qs.fields(**self._get_task_entries_projection(max_task_entries))
|
||||
queue = qs.first()
|
||||
if not queue:
|
||||
raise errors.bad_request.InvalidQueueId(**query)
|
||||
@@ -112,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
|
||||
@@ -120,16 +143,50 @@ class QueueBLL(object):
|
||||
"""
|
||||
with translate_errors_context():
|
||||
queue = self.get_by_id(company_id=company_id, queue_id=queue_id)
|
||||
if queue.entries and not force:
|
||||
raise errors.bad_request.QueueNotEmpty(
|
||||
"use force=true to delete", id=queue_id
|
||||
)
|
||||
if queue.entries:
|
||||
if not force:
|
||||
raise errors.bad_request.QueueNotEmpty(
|
||||
"use force=true to delete", id=queue_id
|
||||
)
|
||||
from apiserver.bll.task import ChangeStatusRequest
|
||||
|
||||
for item in queue.entries:
|
||||
try:
|
||||
task = Task.get_for_writing(
|
||||
company=company_id,
|
||||
id=item.task,
|
||||
_only=[
|
||||
"id",
|
||||
"company",
|
||||
"status",
|
||||
"enqueue_status",
|
||||
"project",
|
||||
],
|
||||
)
|
||||
if not task:
|
||||
continue
|
||||
|
||||
ChangeStatusRequest(
|
||||
task=task,
|
||||
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(
|
||||
f"Failed dequeuing task {item.task} from queue: {queue_id}"
|
||||
)
|
||||
|
||||
queue.delete()
|
||||
|
||||
def get_all(
|
||||
self,
|
||||
company_id: str,
|
||||
query_dict: dict,
|
||||
query: Q = None,
|
||||
max_task_entries: int = None,
|
||||
ret_params: dict = None,
|
||||
) -> Sequence[dict]:
|
||||
"""Get all the queues according to the query"""
|
||||
@@ -138,13 +195,26 @@ class QueueBLL(object):
|
||||
company=company_id,
|
||||
parameters=query_dict,
|
||||
query_dict=query_dict,
|
||||
query=query,
|
||||
projection_fields=self._get_task_entries_projection(max_task_entries)
|
||||
if max_task_entries
|
||||
else None,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
|
||||
def check_for_workers(self, company_id: str, queue_id: str) -> bool:
|
||||
for worker in self.worker_bll.get_all(company_id):
|
||||
if queue_id in worker.queues:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_queue_infos(
|
||||
self,
|
||||
company_id: str,
|
||||
query_dict: dict,
|
||||
query: Q = None,
|
||||
max_task_entries: int = None,
|
||||
ret_params: dict = None,
|
||||
) -> Sequence[dict]:
|
||||
"""
|
||||
@@ -155,7 +225,11 @@ class QueueBLL(object):
|
||||
res = Queue.get_many_with_join(
|
||||
company=company_id,
|
||||
query_dict=query_dict,
|
||||
query=query,
|
||||
override_projection=projection,
|
||||
projection_fields=self._get_task_entries_projection(max_task_entries)
|
||||
if max_task_entries
|
||||
else None,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
|
||||
@@ -169,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"], [])
|
||||
@@ -203,16 +278,22 @@ class QueueBLL(object):
|
||||
|
||||
return res
|
||||
|
||||
def get_next_task(self, company_id: str, queue_id: str) -> Optional[Entry]:
|
||||
def get_next_task(
|
||||
self, company_id: str, queue_id: str, task_id: str = None
|
||||
) -> Optional[Entry]:
|
||||
"""
|
||||
Atomically pop and return the first task from the queue (or None)
|
||||
:raise errors.bad_request.InvalidQueueId: if the queue does not exist
|
||||
"""
|
||||
with translate_errors_context():
|
||||
query = dict(id=queue_id, company=company_id)
|
||||
queue = Queue.objects(**query).modify(pop__entries=-1, upsert=False)
|
||||
queue = Queue.objects(
|
||||
**query, **({"entries__0__task": task_id} if task_id else {})
|
||||
).modify(pop__entries=-1, upsert=False)
|
||||
if not queue:
|
||||
raise errors.bad_request.InvalidQueueId(**query)
|
||||
if not task_id or not Queue.objects(**query).first():
|
||||
raise errors.bad_request.InvalidQueueId(**query)
|
||||
return
|
||||
|
||||
self.metrics.log_queue_metrics_to_es(company_id, queues=[queue])
|
||||
|
||||
@@ -248,43 +329,147 @@ 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(
|
||||
Queue.aggregate(
|
||||
[
|
||||
{
|
||||
"$match": {
|
||||
"company": {"$in": [None, "", company]},
|
||||
"_id": queue_id,
|
||||
}
|
||||
},
|
||||
{"$project": {"count": {"$size": "$entries"}}},
|
||||
]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if res is None:
|
||||
raise errors.bad_request.InvalidQueueId(queue_id=queue_id)
|
||||
return int(res.get("count"))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
from typing import Sequence
|
||||
|
||||
import elasticsearch.helpers
|
||||
from boltons.typeutils import classproperty
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
from apiserver.es_factory import es_factory
|
||||
@@ -11,25 +13,30 @@ from apiserver.bll.query import Builder as QueryBuilder
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.queue import Queue, Entry
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.redis_manager import redman
|
||||
from apiserver.utilities.threads_manager import ThreadsManager
|
||||
|
||||
log = config.logger(__file__)
|
||||
_conf = config.get("services.queues")
|
||||
_queue_metrics_key_pattern = "queue_metrics_{queue}"
|
||||
redis = redman.connection("apiserver")
|
||||
|
||||
|
||||
class EsKeys:
|
||||
WAITING_TIME_FIELD = "average_waiting_time"
|
||||
QUEUE_LENGTH_FIELD = "queue_length"
|
||||
TIMESTAMP_FIELD = "timestamp"
|
||||
QUEUE_FIELD = "queue"
|
||||
|
||||
|
||||
class QueueMetrics:
|
||||
class EsKeys:
|
||||
WAITING_TIME_FIELD = "average_waiting_time"
|
||||
QUEUE_LENGTH_FIELD = "queue_length"
|
||||
TIMESTAMP_FIELD = "timestamp"
|
||||
QUEUE_FIELD = "queue"
|
||||
|
||||
def __init__(self, es: Elasticsearch):
|
||||
self.es = es
|
||||
|
||||
@staticmethod
|
||||
def _queue_metrics_prefix_for_company(company_id: str) -> str:
|
||||
"""Returns the es index prefix for the company"""
|
||||
return f"queue_metrics_{company_id}_"
|
||||
return f"queue_metrics_{company_id.lower()}_"
|
||||
|
||||
@staticmethod
|
||||
def _get_es_index_suffix():
|
||||
@@ -49,7 +56,7 @@ class QueueMetrics:
|
||||
total_waiting_in_secs = sum((now - e.added).total_seconds() for e in entries)
|
||||
return total_waiting_in_secs / len(entries)
|
||||
|
||||
def log_queue_metrics_to_es(self, company_id: str, queues: Sequence[Queue]) -> bool:
|
||||
def log_queue_metrics_to_es(self, company_id: str, queues: Sequence[Queue]) -> int:
|
||||
"""
|
||||
Calculate and write queue statistics (avg waiting time and queue length) to Elastic
|
||||
:return: True if the write to es was successful, false otherwise
|
||||
@@ -63,23 +70,22 @@ class QueueMetrics:
|
||||
|
||||
def make_doc(queue: Queue) -> dict:
|
||||
entries = [e for e in queue.entries if e.added]
|
||||
return dict(
|
||||
_index=es_index,
|
||||
_source={
|
||||
self.EsKeys.TIMESTAMP_FIELD: timestamp,
|
||||
self.EsKeys.QUEUE_FIELD: queue.id,
|
||||
self.EsKeys.WAITING_TIME_FIELD: self._calc_avg_waiting_time(
|
||||
entries
|
||||
),
|
||||
self.EsKeys.QUEUE_LENGTH_FIELD: len(entries),
|
||||
},
|
||||
)
|
||||
return {
|
||||
EsKeys.TIMESTAMP_FIELD: timestamp,
|
||||
EsKeys.QUEUE_FIELD: queue.id,
|
||||
EsKeys.WAITING_TIME_FIELD: self._calc_avg_waiting_time(entries),
|
||||
EsKeys.QUEUE_LENGTH_FIELD: len(entries),
|
||||
}
|
||||
|
||||
actions = list(map(make_doc, queues))
|
||||
logged = 0
|
||||
for q in queues:
|
||||
queue_doc = make_doc(q)
|
||||
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
|
||||
|
||||
es_res = elasticsearch.helpers.bulk(self.es, actions)
|
||||
added, errors = es_res[:2]
|
||||
return (added == len(actions)) and not errors
|
||||
return logged
|
||||
|
||||
def _log_current_metrics(self, company_id: str, queue_ids=Sequence[str]):
|
||||
query = dict(company=company_id)
|
||||
@@ -90,8 +96,7 @@ class QueueMetrics:
|
||||
|
||||
def _search_company_metrics(self, company_id: str, es_req: dict) -> dict:
|
||||
return self.es.search(
|
||||
index=f"{self._queue_metrics_prefix_for_company(company_id)}*",
|
||||
body=es_req,
|
||||
index=f"{self._queue_metrics_prefix_for_company(company_id)}*", body=es_req,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -105,13 +110,13 @@ class QueueMetrics:
|
||||
return {
|
||||
"dates": {
|
||||
"date_histogram": {
|
||||
"field": cls.EsKeys.TIMESTAMP_FIELD,
|
||||
"field": EsKeys.TIMESTAMP_FIELD,
|
||||
"fixed_interval": f"{interval}s",
|
||||
"min_doc_count": 1,
|
||||
},
|
||||
"aggs": {
|
||||
"queues": {
|
||||
"terms": {"field": cls.EsKeys.QUEUE_FIELD},
|
||||
"terms": {"field": EsKeys.QUEUE_FIELD},
|
||||
"aggs": cls._get_top_waiting_agg(),
|
||||
}
|
||||
},
|
||||
@@ -128,13 +133,13 @@ class QueueMetrics:
|
||||
"top_avg_waiting": {
|
||||
"top_hits": {
|
||||
"sort": [
|
||||
{cls.EsKeys.WAITING_TIME_FIELD: {"order": "desc"}},
|
||||
{cls.EsKeys.QUEUE_LENGTH_FIELD: {"order": "desc"}},
|
||||
{EsKeys.WAITING_TIME_FIELD: {"order": "desc"}},
|
||||
{EsKeys.QUEUE_LENGTH_FIELD: {"order": "desc"}},
|
||||
],
|
||||
"_source": {
|
||||
"includes": [
|
||||
cls.EsKeys.WAITING_TIME_FIELD,
|
||||
cls.EsKeys.QUEUE_LENGTH_FIELD,
|
||||
EsKeys.WAITING_TIME_FIELD,
|
||||
EsKeys.QUEUE_LENGTH_FIELD,
|
||||
]
|
||||
},
|
||||
"size": 1,
|
||||
@@ -149,6 +154,7 @@ class QueueMetrics:
|
||||
to_date: float,
|
||||
interval: int,
|
||||
queue_ids: Sequence[str],
|
||||
refresh: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Get the company queue metrics in the specified time range.
|
||||
@@ -158,7 +164,8 @@ class QueueMetrics:
|
||||
In case no queue ids are specified the avg across all the
|
||||
company queues is calculated for each metric
|
||||
"""
|
||||
# self._log_current_metrics(company, queue_ids=queue_ids)
|
||||
if refresh:
|
||||
self._log_current_metrics(company_id, queue_ids=queue_ids)
|
||||
|
||||
if from_date >= to_date:
|
||||
raise bad_request.FieldsValueError("from_date must be less than to_date")
|
||||
@@ -174,7 +181,7 @@ class QueueMetrics:
|
||||
"aggs": self._get_dates_agg(interval),
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "get_queue_metrics"):
|
||||
with translate_errors_context():
|
||||
res = self._search_company_metrics(company_id, es_req)
|
||||
|
||||
if "aggregations" not in res:
|
||||
@@ -256,7 +263,52 @@ class QueueMetrics:
|
||||
continue
|
||||
res = queue_data["top_avg_waiting"]["hits"]["hits"][0]["_source"]
|
||||
queue_metrics[queue_data["key"]] = {
|
||||
"queue_length": res[cls.EsKeys.QUEUE_LENGTH_FIELD],
|
||||
"avg_waiting_time": res[cls.EsKeys.WAITING_TIME_FIELD],
|
||||
"queue_length": res[EsKeys.QUEUE_LENGTH_FIELD],
|
||||
"avg_waiting_time": res[EsKeys.WAITING_TIME_FIELD],
|
||||
}
|
||||
return queue_metrics
|
||||
|
||||
|
||||
class MetricsRefresher:
|
||||
threads = ThreadsManager()
|
||||
|
||||
@classproperty
|
||||
def watch_interval_sec(self):
|
||||
return _conf.get("metrics_refresh_interval_sec", 300)
|
||||
|
||||
@classmethod
|
||||
@threads.register("queue_metrics_refresh_watchdog", daemon=True)
|
||||
def start(cls, queue_metrics: QueueMetrics = None):
|
||||
if not cls.watch_interval_sec:
|
||||
return
|
||||
|
||||
if not queue_metrics:
|
||||
from .queue_bll import QueueBLL
|
||||
|
||||
queue_metrics = QueueBLL().metrics
|
||||
|
||||
sleep(10)
|
||||
while True:
|
||||
try:
|
||||
for queue in Queue.objects():
|
||||
timestamp = es_factory.get_timestamp_millis()
|
||||
doc_time = 0
|
||||
try:
|
||||
redis_key = _queue_metrics_key_pattern.format(queue=queue.id)
|
||||
data = redis.get(redis_key)
|
||||
if data:
|
||||
queue_doc = json.loads(data)
|
||||
doc_time = int(queue_doc.get(EsKeys.TIMESTAMP_FIELD))
|
||||
except Exception as ex:
|
||||
log.exception(
|
||||
f"Error reading queue metrics data for queue {queue.id}: {str(ex)}"
|
||||
)
|
||||
|
||||
if (
|
||||
not doc_time
|
||||
or (timestamp - doc_time) > cls.watch_interval_sec * 1000
|
||||
):
|
||||
queue_metrics.log_queue_metrics_to_es(queue.company, [queue])
|
||||
except Exception as ex:
|
||||
log.exception(f"Failed collecting queue metrics: {str(ex)}")
|
||||
sleep(60)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Optional, TypeVar, Generic, Type, Callable
|
||||
from redis import StrictRedis
|
||||
|
||||
from apiserver import database
|
||||
from apiserver.timing_context import TimingContext
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -31,20 +30,17 @@ class RedisCacheManager(Generic[T]):
|
||||
|
||||
def set_state(self, state: T) -> None:
|
||||
redis_key = self._get_redis_key(state.id)
|
||||
with TimingContext("redis", "cache_set_state"):
|
||||
self.redis.set(redis_key, state.to_json())
|
||||
self.redis.expire(redis_key, self.expiration_interval)
|
||||
self.redis.set(redis_key, state.to_json())
|
||||
self.redis.expire(redis_key, self.expiration_interval)
|
||||
|
||||
def get_state(self, state_id) -> Optional[T]:
|
||||
redis_key = self._get_redis_key(state_id)
|
||||
with TimingContext("redis", "cache_get_state"):
|
||||
response = self.redis.get(redis_key)
|
||||
response = self.redis.get(redis_key)
|
||||
if response:
|
||||
return self.state_class.from_json(response)
|
||||
|
||||
def delete_state(self, state_id) -> None:
|
||||
with TimingContext("redis", "cache_delete_state"):
|
||||
self.redis.delete(self._get_redis_key(state_id))
|
||||
self.redis.delete(self._get_redis_key(state_id))
|
||||
|
||||
def _get_redis_key(self, state_id):
|
||||
return f"{self.state_class}/{state_id}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
import operator
|
||||
from threading import Thread, Lock
|
||||
from threading import Lock
|
||||
from time import sleep
|
||||
|
||||
import attr
|
||||
@@ -9,76 +9,83 @@ import psutil
|
||||
from apiserver.utilities.threads_manager import ThreadsManager
|
||||
|
||||
|
||||
class ResourceMonitor(Thread):
|
||||
@attr.s(auto_attribs=True)
|
||||
class Sample:
|
||||
cpu_usage: float = 0.0
|
||||
mem_used_gb: float = 0
|
||||
mem_free_gb: float = 0
|
||||
stat_threads = ThreadsManager("Statistics")
|
||||
|
||||
@classmethod
|
||||
def _apply(cls, op, *samples):
|
||||
return cls(
|
||||
**{
|
||||
field: op(*(getattr(sample, field) for sample in samples))
|
||||
for field in attr.fields_dict(cls)
|
||||
}
|
||||
)
|
||||
|
||||
def min(self, sample):
|
||||
return self._apply(min, self, sample)
|
||||
|
||||
def max(self, sample):
|
||||
return self._apply(max, self, sample)
|
||||
|
||||
def avg(self, sample, count):
|
||||
res = self._apply(lambda x: x * count, self)
|
||||
res = self._apply(operator.add, res, sample)
|
||||
res = self._apply(lambda x: x / (count + 1), res)
|
||||
return res
|
||||
|
||||
def __init__(self, sample_interval_sec=5):
|
||||
super(ResourceMonitor, self).__init__(daemon=True)
|
||||
self.sample_interval_sec = sample_interval_sec
|
||||
self._lock = Lock()
|
||||
self._clear()
|
||||
|
||||
def _clear(self):
|
||||
sample = self._get_sample()
|
||||
self._avg = sample
|
||||
self._min = sample
|
||||
self._max = sample
|
||||
self._clear_time = datetime.utcnow()
|
||||
self._count = 1
|
||||
@attr.s(auto_attribs=True)
|
||||
class Sample:
|
||||
cpu_usage: float = 0.0
|
||||
mem_used_gb: float = 0
|
||||
mem_free_gb: float = 0
|
||||
|
||||
@classmethod
|
||||
def _get_sample(cls) -> Sample:
|
||||
return cls.Sample(
|
||||
def _apply(cls, op, *samples):
|
||||
return cls(
|
||||
**{
|
||||
field: op(*(getattr(sample, field) for sample in samples))
|
||||
for field in attr.fields_dict(cls)
|
||||
}
|
||||
)
|
||||
|
||||
def min(self, sample):
|
||||
return self._apply(min, self, sample)
|
||||
|
||||
def max(self, sample):
|
||||
return self._apply(max, self, sample)
|
||||
|
||||
def avg(self, sample, count):
|
||||
res = self._apply(lambda x: x * count, self)
|
||||
res = self._apply(operator.add, res, sample)
|
||||
res = self._apply(lambda x: x / (count + 1), res)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_current_sample(cls) -> "Sample":
|
||||
return cls(
|
||||
cpu_usage=psutil.cpu_percent(),
|
||||
mem_used_gb=psutil.virtual_memory().used / (1024 ** 3),
|
||||
mem_free_gb=psutil.virtual_memory().free / (1024 ** 3),
|
||||
)
|
||||
|
||||
def run(self):
|
||||
while not ThreadsManager.terminating:
|
||||
sleep(self.sample_interval_sec)
|
||||
|
||||
sample = self._get_sample()
|
||||
class ResourceMonitor:
|
||||
class Accumulator:
|
||||
def __init__(self):
|
||||
sample = Sample.get_current_sample()
|
||||
self.avg = sample
|
||||
self.min = sample
|
||||
self.max = sample
|
||||
self.time = datetime.utcnow()
|
||||
self.count = 1
|
||||
|
||||
with self._lock:
|
||||
self._min = self._min.min(sample)
|
||||
self._max = self._max.max(sample)
|
||||
self._avg = self._avg.avg(sample, self._count)
|
||||
self._count += 1
|
||||
def add_sample(self, sample: Sample):
|
||||
self.min = self.min.min(sample)
|
||||
self.max = self.max.max(sample)
|
||||
self.avg = self.avg.avg(sample, self.count)
|
||||
self.count += 1
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
sample_interval_sec = 5
|
||||
_lock = Lock()
|
||||
accumulator = Accumulator()
|
||||
|
||||
@classmethod
|
||||
@stat_threads.register("resource_monitor", daemon=True)
|
||||
def start(cls):
|
||||
while True:
|
||||
sleep(cls.sample_interval_sec)
|
||||
sample = Sample.get_current_sample()
|
||||
with cls._lock:
|
||||
cls.accumulator.add_sample(sample)
|
||||
|
||||
@classmethod
|
||||
def get_stats(cls) -> dict:
|
||||
""" Returns current resource statistics and clears internal resource statistics """
|
||||
with self._lock:
|
||||
min_ = attr.asdict(self._min)
|
||||
max_ = attr.asdict(self._max)
|
||||
avg = attr.asdict(self._avg)
|
||||
interval = datetime.utcnow() - self._clear_time
|
||||
self._clear()
|
||||
with cls._lock:
|
||||
min_ = attr.asdict(cls.accumulator.min)
|
||||
max_ = attr.asdict(cls.accumulator.max)
|
||||
avg = attr.asdict(cls.accumulator.avg)
|
||||
interval = datetime.utcnow() - cls.accumulator.time
|
||||
cls.accumulator = cls.Accumulator()
|
||||
|
||||
return {
|
||||
"interval_sec": interval.total_seconds(),
|
||||
|
||||
@@ -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
|
||||
@@ -21,9 +20,8 @@ from apiserver.database.model.queue import Queue
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.tools import safe_get
|
||||
from apiserver.utilities.json import dumps
|
||||
from apiserver.utilities.threads_manager import ThreadsManager
|
||||
from apiserver.version import __version__ as current_version
|
||||
from .resource_monitor import ResourceMonitor
|
||||
from .resource_monitor import ResourceMonitor, stat_threads
|
||||
|
||||
log = config.logger(__file__)
|
||||
|
||||
@@ -31,17 +29,19 @@ worker_bll = WorkerBLL()
|
||||
|
||||
|
||||
class StatisticsReporter:
|
||||
threads = ThreadsManager("Statistics", resource_monitor=ResourceMonitor)
|
||||
send_queue = queue.Queue()
|
||||
supported = config.get("apiserver.statistics.supported", True)
|
||||
|
||||
@classmethod
|
||||
def start(cls):
|
||||
if not cls.supported:
|
||||
return
|
||||
ResourceMonitor.start()
|
||||
cls.start_sender()
|
||||
cls.start_reporter()
|
||||
|
||||
@classmethod
|
||||
@threads.register("reporter", daemon=True)
|
||||
@stat_threads.register("reporter", daemon=True)
|
||||
def start_reporter(cls):
|
||||
"""
|
||||
Periodically send statistics reports for companies who have opted in.
|
||||
@@ -54,7 +54,7 @@ class StatisticsReporter:
|
||||
hours=config.get("apiserver.statistics.report_interval_hours", 24)
|
||||
)
|
||||
sleep(report_interval.total_seconds())
|
||||
while not ThreadsManager.terminating:
|
||||
while True:
|
||||
try:
|
||||
for company in Company.objects(
|
||||
defaults__stats_option__enabled=True
|
||||
@@ -68,7 +68,7 @@ class StatisticsReporter:
|
||||
sleep(report_interval.total_seconds())
|
||||
|
||||
@classmethod
|
||||
@threads.register("sender", daemon=True)
|
||||
@stat_threads.register("sender", daemon=True)
|
||||
def start_sender(cls):
|
||||
if not cls.supported:
|
||||
return
|
||||
@@ -85,7 +85,7 @@ class StatisticsReporter:
|
||||
|
||||
WarningFilter.attach()
|
||||
|
||||
while not ThreadsManager.terminating:
|
||||
while True:
|
||||
try:
|
||||
report = cls.send_queue.get()
|
||||
|
||||
@@ -111,7 +111,7 @@ class StatisticsReporter:
|
||||
"uuid": get_server_uuid(),
|
||||
"queues": {"count": Queue.objects(company=company_id).count()},
|
||||
"users": {"count": User.objects(company=company_id).count()},
|
||||
"resources": cls.threads.resource_monitor.get_stats(),
|
||||
"resources": ResourceMonitor.get_stats(),
|
||||
"experiments": next(
|
||||
iter(cls._get_experiments_stats(company_id).values()), {}
|
||||
),
|
||||
|
||||
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)
|
||||
@@ -5,7 +5,6 @@ from apiserver.apimodels.tasks import Artifact as ApiArtifact, ArtifactId
|
||||
from apiserver.bll.task.utils import get_task_for_update, update_task
|
||||
from apiserver.database.model.task.task import DEFAULT_ARTIFACT_MODE, Artifact
|
||||
from apiserver.database.utils import hash_field_name
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.utilities.dicts import nested_get, nested_set
|
||||
from apiserver.utilities.parameter_key_escaper import mongoengine_safe
|
||||
|
||||
@@ -49,49 +48,41 @@ class Artifacts:
|
||||
def add_or_update_artifacts(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
artifacts: Sequence[ApiArtifact],
|
||||
force: bool,
|
||||
) -> int:
|
||||
with TimingContext("mongo", "update_artifacts"):
|
||||
task = get_task_for_update(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
force=force,
|
||||
)
|
||||
task = get_task_for_update(company_id=company_id, task_id=task_id, force=force,)
|
||||
|
||||
artifacts = {
|
||||
get_artifact_id(a): Artifact(**a)
|
||||
for a in (api_artifact.to_struct() for api_artifact in artifacts)
|
||||
}
|
||||
artifacts = {
|
||||
get_artifact_id(a): Artifact(**a)
|
||||
for a in (api_artifact.to_struct() for api_artifact in artifacts)
|
||||
}
|
||||
|
||||
update_cmds = {
|
||||
f"set__execution__artifacts__{mongoengine_safe(name)}": value
|
||||
for name, value in artifacts.items()
|
||||
}
|
||||
return update_task(task, update_cmds=update_cmds)
|
||||
update_cmds = {
|
||||
f"set__execution__artifacts__{mongoengine_safe(name)}": value
|
||||
for name, value in artifacts.items()
|
||||
}
|
||||
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,
|
||||
) -> int:
|
||||
with TimingContext("mongo", "delete_artifacts"):
|
||||
task = get_task_for_update(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
force=force,
|
||||
)
|
||||
task = get_task_for_update(company_id=company_id, task_id=task_id, force=force,)
|
||||
|
||||
artifact_ids = [
|
||||
get_artifact_id(a)
|
||||
for a in (artifact_id.to_struct() for artifact_id in artifact_ids)
|
||||
]
|
||||
delete_cmds = {
|
||||
f"unset__execution__artifacts__{id_}": 1 for id_ in set(artifact_ids)
|
||||
}
|
||||
artifact_ids = [
|
||||
get_artifact_id(a)
|
||||
for a in (artifact_id.to_struct() for artifact_id in artifact_ids)
|
||||
]
|
||||
delete_cmds = {
|
||||
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)
|
||||
|
||||
@@ -15,7 +15,6 @@ from apiserver.bll.task import TaskBLL
|
||||
from apiserver.bll.task.utils import get_task_for_update, update_task
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model.task.task import ParamsItem, Task, ConfigurationItem
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.utilities.parameter_key_escaper import (
|
||||
ParameterKeyEscaper,
|
||||
mongoengine_safe,
|
||||
@@ -64,78 +63,82 @@ class HyperParams:
|
||||
def delete_params(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
hyperparams: Sequence[HyperParamKey],
|
||||
force: bool,
|
||||
) -> int:
|
||||
with TimingContext("mongo", "delete_hyperparams"):
|
||||
properties_only = cls._normalize_params(hyperparams)
|
||||
task = get_task_for_update(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
allow_all_statuses=properties_only,
|
||||
force=force,
|
||||
)
|
||||
properties_only = cls._normalize_params(hyperparams)
|
||||
task = get_task_for_update(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
allow_all_statuses=properties_only,
|
||||
force=force,
|
||||
)
|
||||
|
||||
with_param, without_param = iterutils.partition(
|
||||
hyperparams, key=lambda p: bool(p.name)
|
||||
)
|
||||
sections_to_delete = {p.section for p in without_param}
|
||||
delete_cmds = {
|
||||
f"unset__hyperparams__{ParameterKeyEscaper.escape(section)}": 1
|
||||
for section in sections_to_delete
|
||||
}
|
||||
with_param, without_param = iterutils.partition(
|
||||
hyperparams, key=lambda p: bool(p.name)
|
||||
)
|
||||
sections_to_delete = {p.section for p in without_param}
|
||||
delete_cmds = {
|
||||
f"unset__hyperparams__{ParameterKeyEscaper.escape(section)}": 1
|
||||
for section in sections_to_delete
|
||||
}
|
||||
|
||||
for item in with_param:
|
||||
section = ParameterKeyEscaper.escape(item.section)
|
||||
if item.section in sections_to_delete:
|
||||
raise errors.bad_request.FieldsConflict(
|
||||
"Cannot delete section field if the whole section was scheduled for deletion"
|
||||
)
|
||||
name = ParameterKeyEscaper.escape(item.name)
|
||||
delete_cmds[f"unset__hyperparams__{section}__{name}"] = 1
|
||||
for item in with_param:
|
||||
section = ParameterKeyEscaper.escape(item.section)
|
||||
if item.section in sections_to_delete:
|
||||
raise errors.bad_request.FieldsConflict(
|
||||
"Cannot delete section field if the whole section was scheduled for deletion"
|
||||
)
|
||||
name = ParameterKeyEscaper.escape(item.name)
|
||||
delete_cmds[f"unset__hyperparams__{section}__{name}"] = 1
|
||||
|
||||
return update_task(
|
||||
task, update_cmds=delete_cmds, set_last_update=not properties_only
|
||||
)
|
||||
return update_task(
|
||||
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,
|
||||
force: bool,
|
||||
) -> int:
|
||||
with TimingContext("mongo", "edit_hyperparams"):
|
||||
properties_only = cls._normalize_params(hyperparams)
|
||||
task = get_task_for_update(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
allow_all_statuses=properties_only,
|
||||
force=force,
|
||||
)
|
||||
properties_only = cls._normalize_params(hyperparams)
|
||||
task = get_task_for_update(
|
||||
company_id=company_id,
|
||||
task_id=task_id,
|
||||
allow_all_statuses=properties_only,
|
||||
force=force,
|
||||
)
|
||||
|
||||
update_cmds = dict()
|
||||
hyperparams = cls._db_dicts_from_list(hyperparams)
|
||||
if replace_hyperparams == ReplaceHyperparams.all:
|
||||
update_cmds["set__hyperparams"] = hyperparams
|
||||
elif replace_hyperparams == ReplaceHyperparams.section:
|
||||
for section, value in hyperparams.items():
|
||||
update_cmds = dict()
|
||||
hyperparams = cls._db_dicts_from_list(hyperparams)
|
||||
if replace_hyperparams == ReplaceHyperparams.all:
|
||||
update_cmds["set__hyperparams"] = hyperparams
|
||||
elif replace_hyperparams == ReplaceHyperparams.section:
|
||||
for section, value in hyperparams.items():
|
||||
update_cmds[f"set__hyperparams__{mongoengine_safe(section)}"] = value
|
||||
else:
|
||||
for section, section_params in hyperparams.items():
|
||||
for name, value in section_params.items():
|
||||
update_cmds[
|
||||
f"set__hyperparams__{mongoengine_safe(section)}"
|
||||
f"set__hyperparams__{section}__{mongoengine_safe(name)}"
|
||||
] = value
|
||||
else:
|
||||
for section, section_params in hyperparams.items():
|
||||
for name, value in section_params.items():
|
||||
update_cmds[
|
||||
f"set__hyperparams__{section}__{mongoengine_safe(name)}"
|
||||
] = value
|
||||
|
||||
return update_task(
|
||||
task, update_cmds=update_cmds, set_last_update=not properties_only
|
||||
)
|
||||
return update_task(
|
||||
task,
|
||||
user_id=user_id,
|
||||
update_cmds=update_cmds,
|
||||
set_last_update=not properties_only,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _db_dicts_from_list(cls, items: Sequence[HyperParamItem]) -> Dict[str, dict]:
|
||||
@@ -191,57 +194,56 @@ class HyperParams:
|
||||
{"$group": {"_id": "$_id", "names": {"$addToSet": "$items.k"}}},
|
||||
]
|
||||
|
||||
with TimingContext("mongo", "get_configuration_names"):
|
||||
tasks = Task.aggregate(pipeline)
|
||||
tasks = Task.aggregate(pipeline)
|
||||
|
||||
return {
|
||||
task["_id"]: {
|
||||
"names": sorted(
|
||||
ParameterKeyEscaper.unescape(name) for name in task["names"]
|
||||
)
|
||||
}
|
||||
for task in tasks
|
||||
return {
|
||||
task["_id"]: {
|
||||
"names": sorted(
|
||||
ParameterKeyEscaper.unescape(name) for name in task["names"]
|
||||
)
|
||||
}
|
||||
for task in tasks
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def edit_configuration(
|
||||
cls,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
task_id: str,
|
||||
configuration: Sequence[Configuration],
|
||||
replace_configuration: bool,
|
||||
force: bool,
|
||||
) -> int:
|
||||
with TimingContext("mongo", "edit_configuration"):
|
||||
task = get_task_for_update(
|
||||
company_id=company_id, task_id=task_id, force=force
|
||||
)
|
||||
task = get_task_for_update(company_id=company_id, task_id=task_id, force=force)
|
||||
|
||||
update_cmds = dict()
|
||||
configuration = {
|
||||
ParameterKeyEscaper.escape(c.name): ConfigurationItem(**c.to_struct())
|
||||
for c in configuration
|
||||
}
|
||||
if replace_configuration:
|
||||
update_cmds["set__configuration"] = configuration
|
||||
else:
|
||||
for name, value in configuration.items():
|
||||
update_cmds[f"set__configuration__{mongoengine_safe(name)}"] = value
|
||||
update_cmds = dict()
|
||||
configuration = {
|
||||
ParameterKeyEscaper.escape(c.name): ConfigurationItem(**c.to_struct())
|
||||
for c in configuration
|
||||
}
|
||||
if replace_configuration:
|
||||
update_cmds["set__configuration"] = configuration
|
||||
else:
|
||||
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:
|
||||
with TimingContext("mongo", "delete_configuration"):
|
||||
task = get_task_for_update(
|
||||
company_id=company_id, task_id=task_id, force=force
|
||||
)
|
||||
task = get_task_for_update(company_id=company_id, task_id=task_id, force=force)
|
||||
|
||||
delete_cmds = {
|
||||
f"unset__configuration__{ParameterKeyEscaper.escape(name)}": 1
|
||||
for name in set(configuration)
|
||||
}
|
||||
delete_cmds = {
|
||||
f"unset__configuration__{ParameterKeyEscaper.escape(name)}": 1
|
||||
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)
|
||||
|
||||
@@ -39,7 +39,7 @@ class NonResponsiveTasksWatchdog:
|
||||
@threads.register("non_responsive_tasks_watchdog", daemon=True)
|
||||
def start(cls):
|
||||
sleep(cls.settings.watch_interval_sec)
|
||||
while not ThreadsManager.terminating:
|
||||
while True:
|
||||
watch_interval = cls.settings.watch_interval_sec
|
||||
if cls.settings.enabled:
|
||||
try:
|
||||
|
||||
@@ -121,18 +121,31 @@ def params_prepare_for_save(fields: dict, previous_task: Task = None):
|
||||
nested_set(fields, new_path, new_param)
|
||||
nested_delete(fields, old_params_field)
|
||||
|
||||
for param_field in ("hyperparams", "configuration"):
|
||||
params = fields.get(param_field)
|
||||
if params:
|
||||
escaped_params = {
|
||||
ParameterKeyEscaper.escape(key): {
|
||||
ParameterKeyEscaper.escape(k): v for k, v in value.items()
|
||||
}
|
||||
if isinstance(value, dict)
|
||||
else value
|
||||
for key, value in params.items()
|
||||
def ensure_non_empty(k: str, desc: str) -> str:
|
||||
if not k:
|
||||
raise errors.bad_request.ValidationError(
|
||||
f"Empty {desc} name is not allowed"
|
||||
)
|
||||
return k
|
||||
|
||||
params = fields.get("hyperparams")
|
||||
if params:
|
||||
escaped_params = {
|
||||
ParameterKeyEscaper.escape(ensure_non_empty(key, "section")): {
|
||||
ParameterKeyEscaper.escape(ensure_non_empty(k, "parameter")): v
|
||||
for k, v in value.items()
|
||||
}
|
||||
fields[param_field] = escaped_params
|
||||
for key, value in params.items()
|
||||
}
|
||||
fields["hyperparams"] = escaped_params
|
||||
|
||||
params = fields.get("configuration")
|
||||
if params:
|
||||
escaped_params = {
|
||||
ParameterKeyEscaper.escape(ensure_non_empty(key, "configuration")): value
|
||||
for key, value in params.items()
|
||||
}
|
||||
fields["configuration"] = escaped_params
|
||||
|
||||
|
||||
def params_unprepare_from_saved(fields, copy_to_legacy=False):
|
||||
@@ -186,7 +199,7 @@ def escape_paths(paths: Sequence[str]) -> Sequence[str]:
|
||||
for old_prefix, new_prefix in (
|
||||
("execution.parameters", f"hyperparams.{hyperparams_default_section}"),
|
||||
("execution.model_desc", "configuration"),
|
||||
("execution.docker_cmd", "container")
|
||||
("execution.docker_cmd", "container"),
|
||||
):
|
||||
path: str
|
||||
paths = [path.replace(old_prefix, new_prefix) for path in paths]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Collection, Sequence, Tuple, Any, Optional, Dict
|
||||
from typing import Collection, Sequence, Tuple, Optional, Dict
|
||||
|
||||
import six
|
||||
from mongoengine import Q
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
@@ -30,18 +30,18 @@ from apiserver.database.model.task.task import (
|
||||
TaskModelTypes,
|
||||
)
|
||||
from apiserver.database.model import EntityVisibility
|
||||
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 apiserver.timing_context import TimingContext
|
||||
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__)
|
||||
@@ -66,11 +66,10 @@ class TaskBLL:
|
||||
"""
|
||||
with translate_errors_context():
|
||||
query = dict(id=task_id, company=company_id)
|
||||
with TimingContext("mongo", "task_with_access"):
|
||||
if requires_write_access:
|
||||
task = Task.get_for_writing(_only=only, **query)
|
||||
else:
|
||||
task = Task.get(_only=only, **query, include_public=allow_public)
|
||||
if requires_write_access:
|
||||
task = Task.get_for_writing(_only=only, **query)
|
||||
else:
|
||||
task = Task.get(_only=only, **query, include_public=allow_public)
|
||||
|
||||
if not task:
|
||||
raise errors.bad_request.InvalidTaskId(**query)
|
||||
@@ -88,15 +87,14 @@ class TaskBLL:
|
||||
only_fields = list(only_fields)
|
||||
only_fields = only_fields + ["status"]
|
||||
|
||||
with TimingContext("mongo", "task_by_id_all"):
|
||||
tasks = Task.get_many(
|
||||
company=company_id,
|
||||
query=Q(id=task_id),
|
||||
allow_public=allow_public,
|
||||
override_projection=only_fields,
|
||||
return_dicts=False,
|
||||
)
|
||||
task = None if not tasks else tasks[0]
|
||||
tasks = Task.get_many(
|
||||
company=company_id,
|
||||
query=Q(id=task_id),
|
||||
allow_public=allow_public,
|
||||
override_projection=only_fields,
|
||||
return_dicts=False,
|
||||
)
|
||||
task = None if not tasks else tasks[0]
|
||||
|
||||
if not task:
|
||||
raise errors.bad_request.InvalidTaskId(id=task_id)
|
||||
@@ -111,7 +109,7 @@ class TaskBLL:
|
||||
company_id, task_ids, only=None, allow_public=False, return_tasks=True
|
||||
) -> Optional[Sequence[Task]]:
|
||||
task_ids = [task_ids] if isinstance(task_ids, six.string_types) else task_ids
|
||||
with translate_errors_context(), TimingContext("mongo", "task_exists"):
|
||||
with translate_errors_context():
|
||||
ids = set(task_ids)
|
||||
q = Task.get_many(
|
||||
company=company_id,
|
||||
@@ -131,16 +129,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,
|
||||
)
|
||||
|
||||
@@ -260,58 +258,66 @@ class TaskBLL:
|
||||
not in [TaskSystemTags.development, EntityVisibility.archived.value]
|
||||
]
|
||||
|
||||
with TimingContext("mongo", "clone task"):
|
||||
parent_task = (
|
||||
task.parent
|
||||
if task.parent and not task.parent.startswith(deleted_prefix)
|
||||
else None
|
||||
)
|
||||
new_task = Task(
|
||||
id=create_id(),
|
||||
user=user_id,
|
||||
company=company_id,
|
||||
created=now,
|
||||
last_update=now,
|
||||
last_change=now,
|
||||
name=name or task.name,
|
||||
comment=comment or task.comment,
|
||||
parent=parent or parent_task,
|
||||
project=project or task.project,
|
||||
tags=tags or task.tags,
|
||||
system_tags=system_tags or clean_system_tags(task.system_tags),
|
||||
type=task.type,
|
||||
script=task.script,
|
||||
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,
|
||||
configuration=params_dict.get("configuration") or task.configuration,
|
||||
hyperparams=params_dict.get("hyperparams") or task.hyperparams,
|
||||
)
|
||||
cls.validate(
|
||||
new_task,
|
||||
validate_models=validate_references or input_models,
|
||||
validate_parent=validate_references or parent,
|
||||
validate_project=validate_references or project,
|
||||
)
|
||||
new_task.save()
|
||||
def ensure_int_labels(execution: dict) -> dict:
|
||||
if not execution:
|
||||
return execution
|
||||
|
||||
if task.project == new_task.project:
|
||||
updated_tags = tags
|
||||
updated_system_tags = system_tags
|
||||
else:
|
||||
updated_tags = new_task.tags
|
||||
updated_system_tags = new_task.system_tags
|
||||
org_bll.update_tags(
|
||||
company_id,
|
||||
Tags.Task,
|
||||
project=new_task.project,
|
||||
tags=updated_tags,
|
||||
system_tags=updated_system_tags,
|
||||
)
|
||||
update_project_time(new_task.project)
|
||||
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)
|
||||
else task.id
|
||||
)
|
||||
new_task = Task(
|
||||
id=create_id(),
|
||||
user=user_id,
|
||||
company=company_id,
|
||||
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,
|
||||
project=project or task.project,
|
||||
tags=tags or task.tags,
|
||||
system_tags=system_tags or clean_system_tags(task.system_tags),
|
||||
type=task.type,
|
||||
script=task.script,
|
||||
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=ensure_int_labels(execution_dict),
|
||||
configuration=params_dict.get("configuration") or task.configuration,
|
||||
hyperparams=params_dict.get("hyperparams") or task.hyperparams,
|
||||
)
|
||||
cls.validate(
|
||||
new_task,
|
||||
validate_models=validate_references or input_models,
|
||||
validate_parent=validate_references or parent,
|
||||
validate_project=validate_references or project,
|
||||
)
|
||||
new_task.save()
|
||||
|
||||
if task.project == new_task.project:
|
||||
updated_tags = tags
|
||||
updated_system_tags = system_tags
|
||||
else:
|
||||
updated_tags = new_task.tags
|
||||
updated_system_tags = new_task.system_tags
|
||||
org_bll.update_tags(
|
||||
company_id,
|
||||
Tags.Task,
|
||||
project=new_task.project,
|
||||
tags=updated_tags,
|
||||
system_tags=updated_system_tags,
|
||||
)
|
||||
update_project_time(new_task.project)
|
||||
|
||||
return new_task, new_project_data
|
||||
|
||||
@@ -381,7 +387,7 @@ class TaskBLL:
|
||||
last_update: datetime = None,
|
||||
last_iteration: int = None,
|
||||
last_iteration_max: int = None,
|
||||
last_scalar_values: Sequence[Tuple[Tuple[str, ...], Any]] = None,
|
||||
last_scalar_events: Dict[str, Dict[str, dict]] = None,
|
||||
last_events: Dict[str, Dict[str, dict]] = None,
|
||||
**extra_updates,
|
||||
):
|
||||
@@ -406,25 +412,21 @@ class TaskBLL:
|
||||
elif last_iteration_max is not None:
|
||||
extra_updates.update(max__last_iteration=last_iteration_max)
|
||||
|
||||
if last_scalar_values is not None:
|
||||
|
||||
def op_path(op, *path):
|
||||
return "__".join((op, "last_metrics") + path)
|
||||
|
||||
for path, value in last_scalar_values:
|
||||
if path[-1] == "min_value":
|
||||
extra_updates[op_path("min", *path[:-1], "min_value")] = value
|
||||
elif path[-1] == "max_value":
|
||||
extra_updates[op_path("max", *path[:-1], "max_value")] = value
|
||||
else:
|
||||
extra_updates[op_path("set", *path)] = value
|
||||
raw_updates = {}
|
||||
if last_scalar_events is not None:
|
||||
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 = {
|
||||
@@ -435,24 +437,53 @@ 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,
|
||||
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,
|
||||
):
|
||||
cls.dequeue(task, company_id)
|
||||
try:
|
||||
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,64 +1,32 @@
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
from typing import Sequence, Generic, Callable, Type, Iterable, TypeVar, List, Set
|
||||
from typing import Sequence, Set, Tuple
|
||||
|
||||
import attr
|
||||
from boltons.iterutils import partition
|
||||
from mongoengine import QuerySet, Document
|
||||
from boltons.iterutils import partition, bucketize, first
|
||||
from furl import furl
|
||||
from mongoengine import NotUniqueError
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.bll.event import EventBLL
|
||||
from apiserver.bll.event.event_bll import PlotFields
|
||||
from apiserver.bll.event.event_common import EventType
|
||||
from apiserver.bll.task.utils import deleted_prefix
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.task.task import Task, TaskStatus, ArtifactModes
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.database.model.url_to_delete import (
|
||||
StorageType,
|
||||
UrlToDelete,
|
||||
FileType,
|
||||
DeletionStatus,
|
||||
)
|
||||
from apiserver.database.utils import id as db_id
|
||||
|
||||
log = config.logger(__file__)
|
||||
event_bll = EventBLL()
|
||||
T = TypeVar("T", bound=Document)
|
||||
|
||||
|
||||
class DocumentGroup(List[T]):
|
||||
"""
|
||||
Operate on a list of documents as if they were a query result
|
||||
"""
|
||||
|
||||
def __init__(self, document_type: Type[T], documents: Iterable[T]):
|
||||
super(DocumentGroup, self).__init__(documents)
|
||||
self.type = document_type
|
||||
|
||||
@property
|
||||
def ids(self) -> Set[str]:
|
||||
return {obj.id for obj in self}
|
||||
|
||||
def objects(self, *args, **kwargs) -> QuerySet:
|
||||
return self.type.objects(id__in=self.ids, *args, **kwargs)
|
||||
|
||||
|
||||
class TaskOutputs(Generic[T]):
|
||||
"""
|
||||
Split task outputs of the same type by the ready state
|
||||
"""
|
||||
|
||||
published: DocumentGroup[T]
|
||||
draft: DocumentGroup[T]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_published: Callable[[T], bool],
|
||||
document_type: Type[T],
|
||||
children: Iterable[T],
|
||||
):
|
||||
"""
|
||||
:param is_published: predicate returning whether items is considered published
|
||||
:param document_type: type of output
|
||||
:param children: output documents
|
||||
"""
|
||||
self.published, self.draft = map(
|
||||
lambda x: DocumentGroup(document_type, x),
|
||||
partition(children, key=is_published),
|
||||
)
|
||||
async_events_delete = config.get("services.tasks.async_events_delete", False)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@@ -101,64 +69,130 @@ class CleanupResult:
|
||||
)
|
||||
|
||||
|
||||
def collect_plot_image_urls(company: str, task: str) -> Set[str]:
|
||||
def collect_plot_image_urls(company: str, task_or_model: str) -> Set[str]:
|
||||
urls = set()
|
||||
next_scroll_id = None
|
||||
with TimingContext("es", "collect_plot_image_urls"):
|
||||
while True:
|
||||
events, next_scroll_id = event_bll.get_plot_image_urls(
|
||||
company_id=company, task_id=task, 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))
|
||||
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))
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
def collect_debug_image_urls(company: str, task: str) -> Set[str]:
|
||||
def collect_debug_image_urls(company: str, task_or_model: str) -> Set[str]:
|
||||
"""
|
||||
Return the set of unique image urls
|
||||
Uses DebugImagesIterator to make sure that we do not retrieve recycled urls
|
||||
"""
|
||||
metrics = event_bll.get_metrics_and_variants(
|
||||
company_id=company, task_id=task, event_type=EventType.metrics_image
|
||||
)
|
||||
if not metrics:
|
||||
return set()
|
||||
|
||||
task_metrics = {task: {m: [] for m in metrics}}
|
||||
scroll_id = None
|
||||
after_key = None
|
||||
urls = set()
|
||||
while True:
|
||||
res = event_bll.debug_images_iterator.get_task_events(
|
||||
company_id=company,
|
||||
task_metrics=task_metrics,
|
||||
iter_count=10,
|
||||
state_id=scroll_id,
|
||||
res, after_key = event_bll.get_debug_image_urls(
|
||||
company_id=company, task_id=task_or_model, after_key=after_key,
|
||||
)
|
||||
if not res.metric_events or not any(
|
||||
iterations for _, iterations in res.metric_events
|
||||
):
|
||||
urls.update(res)
|
||||
if not after_key:
|
||||
break
|
||||
|
||||
scroll_id = res.next_scroll_id
|
||||
for task, iterations in res.metric_events:
|
||||
urls.update(ev.get("url") for it in iterations for ev in it["events"])
|
||||
|
||||
urls.discard({None})
|
||||
return urls
|
||||
|
||||
|
||||
supported_storage_types = {
|
||||
"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,
|
||||
) -> Set[str]:
|
||||
urls_per_storage = bucketize(
|
||||
urls,
|
||||
key=lambda u: first(
|
||||
type_
|
||||
for prefix, type_ in supported_storage_types.items()
|
||||
if u.startswith(prefix)
|
||||
),
|
||||
)
|
||||
urls_per_storage.pop(None, None)
|
||||
|
||||
processed_urls = set()
|
||||
for storage_type, storage_urls in urls_per_storage.items():
|
||||
delete_folders = (storage_type == StorageType.fileserver) and can_delete_folders
|
||||
scheduled_to_delete = set()
|
||||
for url in storage_urls:
|
||||
folder = None
|
||||
if delete_folders:
|
||||
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:
|
||||
processed_urls.add(url)
|
||||
continue
|
||||
|
||||
try:
|
||||
UrlToDelete(
|
||||
id=db_id(),
|
||||
company=company,
|
||||
user=user,
|
||||
url=to_delete,
|
||||
task=task_id,
|
||||
created=datetime.utcnow(),
|
||||
storage_type=storage_type,
|
||||
type=FileType.folder if folder else FileType.file,
|
||||
).save()
|
||||
except (DuplicateKeyError, NotUniqueError):
|
||||
existing = UrlToDelete.objects(company=company, url=to_delete).first()
|
||||
if existing:
|
||||
existing.update(
|
||||
user=user,
|
||||
task=task_id,
|
||||
created=datetime.utcnow(),
|
||||
retry_count=0,
|
||||
unset__last_failure_time=1,
|
||||
unset__last_failure_reason=1,
|
||||
status=DeletionStatus.created,
|
||||
)
|
||||
processed_urls.add(url)
|
||||
scheduled_to_delete.add(to_delete)
|
||||
|
||||
return processed_urls
|
||||
|
||||
|
||||
def cleanup_task(
|
||||
company: str,
|
||||
user: str,
|
||||
task: Task,
|
||||
force: bool = False,
|
||||
update_children=True,
|
||||
return_file_urls=False,
|
||||
delete_output_models=True,
|
||||
delete_external_artifacts=True,
|
||||
) -> CleanupResult:
|
||||
"""
|
||||
Validate task deletion and delete/modify all its output.
|
||||
@@ -166,10 +200,14 @@ def cleanup_task(
|
||||
:param force: whether to delete task with published outputs
|
||||
:return: count of delete and modified items
|
||||
"""
|
||||
models = verify_task_children_and_ouptuts(task, force)
|
||||
|
||||
published_models, draft_models, in_use_model_ids = verify_task_children_and_ouptuts(
|
||||
task, force
|
||||
)
|
||||
delete_external_artifacts = delete_external_artifacts and config.get(
|
||||
"services.async_urls_delete.enabled", True
|
||||
)
|
||||
event_urls, artifact_urls, model_urls = set(), set(), set()
|
||||
if return_file_urls:
|
||||
if return_file_urls or delete_external_artifacts:
|
||||
event_urls = collect_debug_image_urls(task.company, task.id)
|
||||
event_urls.update(collect_plot_image_urls(task.company, task.id))
|
||||
if task.execution and task.execution.artifacts:
|
||||
@@ -178,30 +216,63 @@ def cleanup_task(
|
||||
for a in task.execution.artifacts.values()
|
||||
if a.mode == ArtifactModes.output and a.uri
|
||||
}
|
||||
model_urls = {m.uri for m in models.draft.objects().only("uri") if m.uri}
|
||||
model_urls = {
|
||||
m.uri for m in draft_models if m.uri and m.id not in in_use_model_ids
|
||||
}
|
||||
|
||||
deleted_task_id = f"{deleted_prefix}{task.id}"
|
||||
updated_children = 0
|
||||
if update_children:
|
||||
with TimingContext("mongo", "update_task_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)
|
||||
|
||||
deleted_models = 0
|
||||
updated_models = 0
|
||||
for models, allow_delete in ((draft_models, True), (published_models, False)):
|
||||
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:
|
||||
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)}")
|
||||
|
||||
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)
|
||||
continue
|
||||
|
||||
if update_children:
|
||||
updated_models += Model.objects(id__in=[m.id for m in models]).update(
|
||||
task=deleted_task_id
|
||||
)
|
||||
else:
|
||||
updated_children = 0
|
||||
else:
|
||||
Model.objects(id__in=[m.id for m in models]).update(unset__task=1)
|
||||
|
||||
if models.draft and delete_output_models:
|
||||
with TimingContext("mongo", "delete_models"):
|
||||
deleted_models = models.draft.objects().delete()
|
||||
else:
|
||||
deleted_models = 0
|
||||
event_bll.delete_task_events(
|
||||
task.company, task.id, allow_locked=force, async_delete=async_events_delete
|
||||
)
|
||||
|
||||
if models.published and update_children:
|
||||
with TimingContext("mongo", "update_task_models"):
|
||||
updated_models = models.published.objects().update(task=deleted_task_id)
|
||||
else:
|
||||
updated_models = 0
|
||||
|
||||
event_bll.delete_task_events(task.company, task.id, allow_locked=force)
|
||||
if delete_external_artifacts:
|
||||
scheduled = _schedule_for_delete(
|
||||
task_id=task.id,
|
||||
company=company,
|
||||
user=user,
|
||||
urls=event_urls | model_urls | artifact_urls,
|
||||
can_delete_folders=not in_use_model_ids and not published_models,
|
||||
)
|
||||
for urls in (event_urls, model_urls, artifact_urls):
|
||||
urls.difference_update(scheduled)
|
||||
|
||||
return CleanupResult(
|
||||
deleted_models=deleted_models,
|
||||
@@ -217,62 +288,56 @@ def cleanup_task(
|
||||
)
|
||||
|
||||
|
||||
def verify_task_children_and_ouptuts(task: Task, force: bool) -> TaskOutputs[Model]:
|
||||
def verify_task_children_and_ouptuts(
|
||||
task, force: bool
|
||||
) -> Tuple[Sequence[Model], Sequence[Model], Set[str]]:
|
||||
if not force:
|
||||
with TimingContext("mongo", "count_published_children"):
|
||||
published_children_count = Task.objects(
|
||||
parent=task.id, status=TaskStatus.published
|
||||
).count()
|
||||
if published_children_count:
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
"has children, use force=True",
|
||||
task=task.id,
|
||||
children=published_children_count,
|
||||
)
|
||||
|
||||
with TimingContext("mongo", "get_task_models"):
|
||||
models = TaskOutputs(
|
||||
attrgetter("ready"),
|
||||
Model,
|
||||
Model.objects(task=task.id).only("id", "task", "ready"),
|
||||
)
|
||||
if not force and models.published:
|
||||
published_children_count = Task.objects(
|
||||
parent=task.id, status=TaskStatus.published
|
||||
).count()
|
||||
if published_children_count:
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
"has output models, use force=True",
|
||||
"has children, use force=True",
|
||||
task=task.id,
|
||||
models=len(models.published),
|
||||
children=published_children_count,
|
||||
)
|
||||
|
||||
model_fields = ["id", "ready", "uri"]
|
||||
published_models, draft_models = partition(
|
||||
Model.objects(task=task.id).only(*model_fields), key=attrgetter("ready"),
|
||||
)
|
||||
if not force and published_models:
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
"has output models, use force=True",
|
||||
task=task.id,
|
||||
models=len(published_models),
|
||||
)
|
||||
|
||||
if task.models and task.models.output:
|
||||
with TimingContext("mongo", "get_task_output_model"):
|
||||
model_ids = [m.model for m in task.models.output]
|
||||
for output_model in Model.objects(id__in=model_ids):
|
||||
if output_model.ready:
|
||||
if not force:
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
"has output model, use force=True",
|
||||
task=task.id,
|
||||
model=output_model.id,
|
||||
)
|
||||
models.published.append(output_model)
|
||||
else:
|
||||
models.draft.append(output_model)
|
||||
model_ids = [m.model for m in task.models.output]
|
||||
for output_model in Model.objects(id__in=model_ids).only(*model_fields):
|
||||
if output_model.ready:
|
||||
if not force:
|
||||
raise errors.bad_request.TaskCannotBeDeleted(
|
||||
"has output model, use force=True",
|
||||
task=task.id,
|
||||
model=output_model.id,
|
||||
)
|
||||
published_models.append(output_model)
|
||||
else:
|
||||
draft_models.append(output_model)
|
||||
|
||||
if models.draft:
|
||||
with TimingContext("mongo", "get_execution_models"):
|
||||
model_ids = models.draft.ids
|
||||
dependent_tasks = Task.objects(models__input__model__in=model_ids).only(
|
||||
"id", "models"
|
||||
in_use_model_ids = {}
|
||||
if draft_models:
|
||||
model_ids = {m.id for m in draft_models}
|
||||
dependent_tasks = Task.objects(models__input__model__in=list(model_ids)).only(
|
||||
"id", "models"
|
||||
)
|
||||
in_use_model_ids = model_ids & {
|
||||
m.model
|
||||
for m in chain.from_iterable(
|
||||
t.models.input for t in dependent_tasks if t.models
|
||||
)
|
||||
input_models = {
|
||||
m.model
|
||||
for m in chain.from_iterable(
|
||||
t.models.input for t in dependent_tasks if t.models
|
||||
)
|
||||
}
|
||||
if input_models:
|
||||
models.draft = DocumentGroup(
|
||||
Model, (m for m in models.draft if m.id not in input_models)
|
||||
)
|
||||
}
|
||||
|
||||
return models
|
||||
return published_models, draft_models, in_use_model_ids
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Callable, Any, Tuple, Union
|
||||
from typing import Callable, Any, Tuple, Union, Sequence
|
||||
|
||||
from apiserver.apierrors import errors, APIError
|
||||
from apiserver.bll.queue import QueueBLL
|
||||
@@ -23,13 +23,19 @@ 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__)
|
||||
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
|
||||
@@ -41,6 +47,7 @@ def archive_task(
|
||||
company_id=company_id,
|
||||
only=(
|
||||
"id",
|
||||
"company",
|
||||
"execution",
|
||||
"status",
|
||||
"project",
|
||||
@@ -51,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
|
||||
@@ -62,11 +74,12 @@ 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
|
||||
@@ -79,25 +92,48 @@ def unarchive_task(
|
||||
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,
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -105,12 +141,27 @@ def dequeue_task(
|
||||
def enqueue_task(
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
queue_id: str,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
queue_name: str = None,
|
||||
validate: bool = False,
|
||||
force: bool = False,
|
||||
) -> Tuple[int, dict]:
|
||||
if queue_id and queue_name:
|
||||
raise errors.bad_request.ValidationError(
|
||||
"Either queue id or queue name should be provided"
|
||||
)
|
||||
|
||||
if queue_name:
|
||||
queue = queue_bll.get_by_name(
|
||||
company_id=company_id, queue_name=queue_name, only=("id",)
|
||||
)
|
||||
if not queue:
|
||||
queue = queue_bll.create(company_id=company_id, name=queue_name)
|
||||
queue_id = queue.id
|
||||
|
||||
if not queue_id:
|
||||
# try to get default queue
|
||||
queue_id = queue_bll.get_default(company_id).id
|
||||
@@ -130,6 +181,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:
|
||||
@@ -142,6 +194,7 @@ def enqueue_task(
|
||||
new_status=task.status,
|
||||
force=True,
|
||||
status_reason="failed enqueueing",
|
||||
user_id=user_id,
|
||||
).execute(enqueue_status=None)
|
||||
raise
|
||||
|
||||
@@ -155,15 +208,41 @@ def enqueue_task(
|
||||
return 1, res
|
||||
|
||||
|
||||
def move_tasks_to_trash(tasks: Sequence[str]) -> int:
|
||||
try:
|
||||
collection_name = Task._get_collection_name()
|
||||
trash_collection_name = f"{collection_name}__trash"
|
||||
Task.aggregate(
|
||||
[
|
||||
{"$match": {"_id": {"$in": tasks}}},
|
||||
{
|
||||
"$merge": {
|
||||
"into": trash_collection_name,
|
||||
"on": "_id",
|
||||
"whenMatched": "replace",
|
||||
"whenNotMatched": "insert",
|
||||
}
|
||||
},
|
||||
],
|
||||
allow_disk_use=True,
|
||||
)
|
||||
except Exception as ex:
|
||||
log.error(f"Error copying tasks to trash {str(ex)}")
|
||||
|
||||
return Task.objects(id__in=tasks).delete()
|
||||
|
||||
|
||||
def delete_task(
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
move_to_trash: bool,
|
||||
force: bool,
|
||||
return_file_urls: bool,
|
||||
delete_output_models: bool,
|
||||
status_message: str,
|
||||
status_reason: str,
|
||||
delete_external_artifacts: bool,
|
||||
) -> Tuple[int, Task, CleanupResult]:
|
||||
task = TaskBLL.get_task_with_access(
|
||||
task_id, company_id=company_id, requires_write_access=True
|
||||
@@ -185,33 +264,33 @@ 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
|
||||
pass
|
||||
|
||||
cleanup_res = cleanup_task(
|
||||
task,
|
||||
company=company_id,
|
||||
user=user_id,
|
||||
task=task,
|
||||
force=force,
|
||||
return_file_urls=return_file_urls,
|
||||
delete_output_models=delete_output_models,
|
||||
delete_external_artifacts=delete_external_artifacts,
|
||||
)
|
||||
|
||||
if move_to_trash:
|
||||
collection_name = task._get_collection_name()
|
||||
archived_collection = "{}__trash".format(collection_name)
|
||||
task.switch_collection(archived_collection)
|
||||
try:
|
||||
# A simple save() won't do due to mongoengine caching (nothing will be saved), so we have to force
|
||||
# an insert. However, if for some reason such an ID exists, let's make sure we'll keep going.
|
||||
task.save(force_insert=True)
|
||||
except Exception:
|
||||
pass
|
||||
task.switch_collection(collection_name)
|
||||
# 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()
|
||||
|
||||
task.delete()
|
||||
update_project_time(task.project)
|
||||
return 1, task, cleanup_res
|
||||
|
||||
@@ -219,10 +298,12 @@ def delete_task(
|
||||
def reset_task(
|
||||
task_id: str,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
force: bool,
|
||||
return_file_urls: bool,
|
||||
delete_output_models: bool,
|
||||
clear_all: bool,
|
||||
delete_external_artifacts: bool,
|
||||
) -> Tuple[dict, CleanupResult, dict]:
|
||||
task = TaskBLL.get_task_with_access(
|
||||
task_id, company_id=company_id, requires_write_access=True
|
||||
@@ -240,17 +321,23 @@ 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(
|
||||
task,
|
||||
company=company_id,
|
||||
user=user_id,
|
||||
task=task,
|
||||
force=force,
|
||||
update_children=False,
|
||||
return_file_urls=return_file_urls,
|
||||
delete_output_models=delete_output_models,
|
||||
delete_external_artifacts=delete_external_artifacts,
|
||||
)
|
||||
|
||||
updates.update(
|
||||
set__last_iteration=DEFAULT_LAST_ITERATION,
|
||||
set__last_metrics={},
|
||||
set__unique_metrics=[],
|
||||
set__metric_stats={},
|
||||
set__models__output=[],
|
||||
set__runtime={},
|
||||
@@ -281,6 +368,7 @@ def reset_task(
|
||||
force=force,
|
||||
status_reason="reset",
|
||||
status_message="reset",
|
||||
user_id=user_id,
|
||||
).execute(
|
||||
started=None,
|
||||
completed=None,
|
||||
@@ -296,8 +384,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:
|
||||
@@ -325,7 +414,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(
|
||||
@@ -334,6 +423,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:
|
||||
@@ -346,7 +436,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
|
||||
@@ -408,4 +503,5 @@ def stop_task(
|
||||
status_reason=status_reason,
|
||||
status_message=status_message,
|
||||
force=force,
|
||||
user_id=user_id,
|
||||
).execute()
|
||||
|
||||
@@ -5,11 +5,12 @@ import attr
|
||||
import six
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.database.model.model import Model
|
||||
from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.task.task import Task, TaskStatus, TaskSystemTags
|
||||
from apiserver.database.utils import get_options
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.utilities.attrs import typed_attrs
|
||||
|
||||
valid_statuses = get_options(TaskStatus)
|
||||
@@ -27,6 +28,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
|
||||
@@ -45,6 +47,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:
|
||||
@@ -55,7 +58,7 @@ class ChangeStatusRequest(object):
|
||||
|
||||
fields.update({safe_mongoengine_key(k): v for k, v in kwargs.items()})
|
||||
|
||||
with translate_errors_context(), TimingContext("mongo", "task_status"):
|
||||
with translate_errors_context():
|
||||
# atomic change of task status by querying the task with the EXPECTED status before modifying it
|
||||
params = fields.copy()
|
||||
params.update(control)
|
||||
@@ -188,9 +191,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,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.apimodels.users import CreateRequest
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
@@ -12,7 +14,7 @@ class UserBLL:
|
||||
if user_id and User.objects(id=user_id).only("id"):
|
||||
raise errors.bad_request.UserIdExists(id=user_id)
|
||||
|
||||
user = User(**request.to_struct())
|
||||
user = User(**request.to_struct(), created=datetime.utcnow())
|
||||
user.save(force_insert=True)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -4,9 +4,6 @@ from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
Dict,
|
||||
Any,
|
||||
Set,
|
||||
Iterable,
|
||||
Tuple,
|
||||
Sequence,
|
||||
@@ -16,61 +13,9 @@ from typing import (
|
||||
from boltons import iterutils
|
||||
|
||||
from apiserver.apierrors import APIError
|
||||
from apiserver.database.model import AttributedDocument
|
||||
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")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import itertools
|
||||
from datetime import datetime, timedelta
|
||||
from time import time
|
||||
from typing import Sequence, Set, Optional
|
||||
|
||||
import attr
|
||||
import elasticsearch.helpers
|
||||
from boltons.iterutils import partition
|
||||
|
||||
from apiserver.es_factory import es_factory
|
||||
from apiserver.apierrors import APIError
|
||||
@@ -25,7 +27,6 @@ from apiserver.database.model.project import Project
|
||||
from apiserver.database.model.queue import Queue
|
||||
from apiserver.database.model.task.task import Task
|
||||
from apiserver.redis_manager import redman
|
||||
from apiserver.timing_context import TimingContext
|
||||
from apiserver.tools import safe_get
|
||||
from .stats import WorkerStats
|
||||
|
||||
@@ -51,6 +52,7 @@ class WorkerBLL:
|
||||
queues: Sequence[str] = None,
|
||||
timeout: int = 0,
|
||||
tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> WorkerEntry:
|
||||
"""
|
||||
Register a worker
|
||||
@@ -76,7 +78,7 @@ class WorkerBLL:
|
||||
raise bad_request.InvalidUserId(**query)
|
||||
company = Company.objects(id=company_id).only("id", "name").first()
|
||||
if not company:
|
||||
raise server_error.InternalError("invalid company", company=company_id)
|
||||
raise bad_request.InvalidId("invalid company", company=company_id)
|
||||
|
||||
queue_objs = Queue.objects(company=company_id, id__in=queues).only("id")
|
||||
if len(queue_objs) < len(queues):
|
||||
@@ -95,9 +97,10 @@ class WorkerBLL:
|
||||
register_timeout=timeout,
|
||||
last_activity_time=now,
|
||||
tags=tags,
|
||||
system_tags=system_tags,
|
||||
)
|
||||
|
||||
self.redis.setex(key, timedelta(seconds=timeout), entry.to_json())
|
||||
self._save_worker_data(entry)
|
||||
|
||||
return entry
|
||||
|
||||
@@ -109,15 +112,20 @@ class WorkerBLL:
|
||||
:param worker: worker ID
|
||||
:raise bad_request.WorkerNotRegistered: the worker was not previously registered
|
||||
"""
|
||||
with TimingContext("redis", "workers_unregister"):
|
||||
res = self.redis.delete(
|
||||
company_id, self._get_worker_key(company_id, user_id, worker)
|
||||
)
|
||||
res = self.redis.delete(
|
||||
company_id, self._get_worker_key(company_id, user_id, worker)
|
||||
)
|
||||
if not res and not config.get("apiserver.workers.auto_unregister", False):
|
||||
raise bad_request.WorkerNotRegistered(worker=worker)
|
||||
|
||||
def status_report(
|
||||
self, company_id: str, user_id: str, ip: str, report: StatusReportRequest, tags: Sequence[str] = None,
|
||||
self,
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
ip: str,
|
||||
report: StatusReportRequest,
|
||||
tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Write worker status report
|
||||
@@ -138,12 +146,14 @@ class WorkerBLL:
|
||||
|
||||
if tags is not None:
|
||||
entry.tags = tags
|
||||
if system_tags is not None:
|
||||
entry.system_tags = system_tags
|
||||
|
||||
if report.machine_stats:
|
||||
self._log_stats_to_es(
|
||||
company_id=company_id,
|
||||
company_name=entry.company.name,
|
||||
worker=report.worker,
|
||||
worker=entry.key,
|
||||
timestamp=report.timestamp,
|
||||
task=report.task,
|
||||
machine_stats=report.machine_stats,
|
||||
@@ -176,7 +186,9 @@ class WorkerBLL:
|
||||
if task.project:
|
||||
project = Project.objects(id=task.project).only("name").first()
|
||||
if project:
|
||||
entry.project = IdNameEntry(id=project.id, name=project.name)
|
||||
entry.project = IdNameEntry(
|
||||
id=project.id, name=project.name
|
||||
)
|
||||
|
||||
entry.last_report_time = now
|
||||
except APIError:
|
||||
@@ -188,8 +200,30 @@ 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, last_seen: Optional[int] = None
|
||||
self,
|
||||
company_id: str,
|
||||
last_seen: Optional[int] = None,
|
||||
tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> Sequence[WorkerEntry]:
|
||||
"""
|
||||
Get all the company workers that were active during the last_seen period
|
||||
@@ -198,7 +232,7 @@ class WorkerBLL:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
workers = self._get(company_id)
|
||||
workers = self._get(company_id, user_tags=tags, system_tags=system_tags)
|
||||
except Exception as e:
|
||||
raise server_error.DataError("failed loading worker entries", err=e.args[0])
|
||||
|
||||
@@ -213,13 +247,22 @@ class WorkerBLL:
|
||||
return workers
|
||||
|
||||
def get_all_with_projection(
|
||||
self, company_id: str, last_seen: int
|
||||
self,
|
||||
company_id: str,
|
||||
last_seen: int,
|
||||
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),
|
||||
self.get_all(
|
||||
company_id=company_id,
|
||||
last_seen=last_seen,
|
||||
tags=tags,
|
||||
system_tags=system_tags,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -310,8 +353,7 @@ class WorkerBLL:
|
||||
"""
|
||||
key = self._get_worker_key(company_id, user_id, worker)
|
||||
|
||||
with TimingContext("redis", "get_worker"):
|
||||
data = self.redis.get(key)
|
||||
data = self.redis.get(key)
|
||||
|
||||
if data:
|
||||
try:
|
||||
@@ -338,24 +380,129 @@ class WorkerBLL:
|
||||
|
||||
raise bad_request.InvalidWorkerId(worker=worker)
|
||||
|
||||
@staticmethod
|
||||
def _get_tagged_workers_key(company: str, tags_field: str, tag: str) -> str:
|
||||
"""Build redis key from company, user and worker_id"""
|
||||
return f"workers.{tags_field}_{company}_{tag}"
|
||||
|
||||
@staticmethod
|
||||
def _get_all_workers_key(company: str) -> str:
|
||||
"""Build redis key from company, user and worker_id"""
|
||||
return f"workers_{company}"
|
||||
|
||||
def _save_worker_data(self, entry: WorkerEntry):
|
||||
self.redis.setex(
|
||||
entry.key, timedelta(seconds=entry.register_timeout), entry.to_json()
|
||||
)
|
||||
company_id = entry.company.id
|
||||
expiration = int(time()) + entry.register_timeout
|
||||
worker_item = {entry.key: expiration}
|
||||
self.redis.zadd(self._get_all_workers_key(company_id), worker_item)
|
||||
for tags, tags_field in (
|
||||
(entry.tags, "tags"),
|
||||
(entry.system_tags, "systemtags"),
|
||||
):
|
||||
for tag in tags:
|
||||
name = self._get_tagged_workers_key(company_id, tags_field, tag)
|
||||
self.redis.zadd(name, worker_item)
|
||||
|
||||
def _save_worker(self, entry: WorkerEntry) -> None:
|
||||
"""Save worker entry in Redis"""
|
||||
try:
|
||||
self.redis.setex(
|
||||
entry.key, timedelta(seconds=entry.register_timeout), entry.to_json()
|
||||
)
|
||||
self._save_worker_data(entry)
|
||||
except Exception:
|
||||
msg = "Failed saving worker entry"
|
||||
log.exception(msg)
|
||||
|
||||
def _get_keys(
|
||||
self,
|
||||
company: str,
|
||||
user: str = "*",
|
||||
user_tags: Sequence[str] = None,
|
||||
system_tags: Sequence[str] = None,
|
||||
) -> 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 == "*":
|
||||
return in_keys
|
||||
user_bytes = user.encode()
|
||||
return {k for k in in_keys if user_bytes in k}
|
||||
|
||||
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 []
|
||||
|
||||
return list(worker_keys)
|
||||
|
||||
def _get(
|
||||
self, company: str, user: str = "*", worker_id: str = "*"
|
||||
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"""
|
||||
match = self._get_worker_key(company, user, worker_id)
|
||||
with TimingContext("redis", "workers_get_all"):
|
||||
res = self.redis.scan_iter(match)
|
||||
return [WorkerEntry.from_json(self.redis.get(r)) for r in res]
|
||||
|
||||
entries = []
|
||||
for key in self._get_keys(
|
||||
company, user=user, user_tags=user_tags, system_tags=system_tags
|
||||
):
|
||||
data = self.redis.get(key)
|
||||
if data:
|
||||
entries.append(WorkerEntry.from_json(data))
|
||||
|
||||
return entries
|
||||
|
||||
@staticmethod
|
||||
def _get_es_index_suffix():
|
||||
|
||||
@@ -8,7 +8,6 @@ from apiserver.apimodels.workers import AggregationType, GetStatsRequest, StatIt
|
||||
from apiserver.bll.query import Builder as QueryBuilder
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.errors import translate_errors_context
|
||||
from apiserver.timing_context import TimingContext
|
||||
|
||||
log = config.logger(__file__)
|
||||
|
||||
@@ -20,7 +19,7 @@ class WorkerStats:
|
||||
@staticmethod
|
||||
def worker_stats_prefix_for_company(company_id: str) -> str:
|
||||
"""Returns the es index prefix for the company"""
|
||||
return f"worker_stats_{company_id}_"
|
||||
return f"worker_stats_{company_id.lower()}_"
|
||||
|
||||
def _search_company_stats(self, company_id: str, es_req: dict) -> dict:
|
||||
return self.es.search(
|
||||
@@ -126,7 +125,7 @@ class WorkerStats:
|
||||
query_terms.append(QueryBuilder.terms("worker", request.worker_ids))
|
||||
es_req["query"] = {"bool": {"must": query_terms}}
|
||||
|
||||
with translate_errors_context(), TimingContext("es", "get_worker_stats"):
|
||||
with translate_errors_context():
|
||||
data = self._search_company_stats(company_id, es_req)
|
||||
|
||||
return self._extract_results(data, request.items, request.split_by_variant)
|
||||
@@ -223,9 +222,7 @@ class WorkerStats:
|
||||
"query": {"bool": {"must": must}},
|
||||
}
|
||||
|
||||
with translate_errors_context(), TimingContext(
|
||||
"es", "get_worker_activity_report"
|
||||
):
|
||||
with translate_errors_context():
|
||||
data = self._search_company_stats(company_id, es_req)
|
||||
|
||||
if "aggregations" not in data:
|
||||
|
||||
@@ -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 {
|
||||
@@ -146,4 +146,11 @@
|
||||
max_backoff_sec: 5
|
||||
}
|
||||
|
||||
getting_started_info {
|
||||
"agentName": "clearml",
|
||||
"configure": "clearml-init",
|
||||
"install": "pip install clearml",
|
||||
"packageName": "clearml"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
fileserver = "http://localhost:8081"
|
||||
|
||||
elastic {
|
||||
events {
|
||||
hosts: [{host: "127.0.0.1", port: 9200}]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
12
apiserver/config/default/services/async_urls_delete.conf
Normal file
12
apiserver/config/default/services/async_urls_delete.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
# 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: true
|
||||
max_retries: 3
|
||||
retry_timeout_sec: 60
|
||||
|
||||
fileserver {
|
||||
# fileserver url prefixes. Evaluated in the order of priority
|
||||
# Can be in the form <schema>://host:port/path or /path
|
||||
url_prefixes: ["https://files.community-master.hosted.allegro.ai/"]
|
||||
timeout_sec: 300
|
||||
}
|
||||
@@ -12,15 +12,28 @@ events_retrieval {
|
||||
# should not exceed the amount of concurrent connections set in the ES driver
|
||||
max_metrics_concurrency: 4
|
||||
|
||||
# If set then max_metrics_count and max_variants_count are calculated dynamically on user data
|
||||
dynamic_metrics_count: true
|
||||
|
||||
# The percentage from the ES aggs limit (10000) to use for the max_metrics and max_variants calculation
|
||||
dynamic_metrics_count_threshold: 80
|
||||
|
||||
# the max amount of metrics to aggregate on
|
||||
max_metrics_count: 100
|
||||
|
||||
# the max amount of variants to aggregate on
|
||||
max_variants_count: 100
|
||||
|
||||
debug_images {
|
||||
# Allow to return the debug images for the variants with uninitialized valid iterations border
|
||||
allow_uninitialized_variants: true
|
||||
}
|
||||
|
||||
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
|
||||
@@ -28,4 +41,7 @@ events_retrieval {
|
||||
validate_plot_str: false
|
||||
|
||||
# If not 0 then the plots equal or greater to the size will be stored compressed in the DB
|
||||
plot_compression_threshold: 100000
|
||||
plot_compression_threshold: 100000
|
||||
|
||||
# async events delete threshold
|
||||
max_async_deleted_events_per_sec: 1000
|
||||
@@ -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
|
||||
}
|
||||
8
apiserver/config/default/services/queues.conf
Normal file
8
apiserver/config/default/services/queues.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
metrics_before_from_date: 3600
|
||||
# interval in seconds to update queue metrics. Put 0 to disable
|
||||
metrics_refresh_interval_sec: 300
|
||||
# the queues with these tags will not be returned from get_all/get_all_ex unless id or name specified
|
||||
# or search_hidden is set
|
||||
hidden_tags: [k8s-glue]
|
||||
}
|
||||
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,12 +11,16 @@ 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
|
||||
|
||||
# cache ttl sec
|
||||
cache_ttl_sec: 86400
|
||||
}
|
||||
}
|
||||
|
||||
# the maximum amount of unique last metrics/variants combinations
|
||||
# for which the last values are stored in a task
|
||||
max_last_metrics: 2000
|
||||
|
||||
# if set then call to tasks.delete/cleanup does not wait for ES events deletion
|
||||
async_events_delete: false
|
||||
|
||||
@@ -29,6 +29,9 @@ OVERRIDE_PORT_ENV_KEY = (
|
||||
)
|
||||
|
||||
OVERRIDE_CONNECTION_STRING_ENV_KEY = "CLEARML_MONGODB_SERVICE_CONNECTION_STRING"
|
||||
OVERRIDE_USERNAME_ENV_KEY = "CLEARML_MONGODB_SERVICE_USERNAME"
|
||||
OVERRIDE_PASSWORD_ENV_KEY = "CLEARML_MONGODB_SERVICE_PASSWORD"
|
||||
OVERRIDE_QUERY_ENV_KEY = "CLEARML_MONGODB_SERVICE_QUERY"
|
||||
|
||||
|
||||
class DatabaseEntry(models.Base):
|
||||
@@ -52,14 +55,23 @@ class DatabaseFactory:
|
||||
override_connection_string = getenv(OVERRIDE_CONNECTION_STRING_ENV_KEY)
|
||||
override_hostname = first(map(getenv, OVERRIDE_HOST_ENV_KEY), None)
|
||||
override_port = first(map(getenv, OVERRIDE_PORT_ENV_KEY), None)
|
||||
override_username = getenv(OVERRIDE_USERNAME_ENV_KEY)
|
||||
override_password = getenv(OVERRIDE_PASSWORD_ENV_KEY)
|
||||
override_query = getenv(OVERRIDE_QUERY_ENV_KEY)
|
||||
|
||||
if override_connection_string:
|
||||
log.info(f"Using override mongodb connection string {override_connection_string}")
|
||||
log.info(f"Using override mongodb connection string template {override_connection_string}")
|
||||
else:
|
||||
if override_hostname:
|
||||
log.info(f"Using override mongodb host {override_hostname}")
|
||||
if override_port:
|
||||
log.info(f"Using override mongodb port {override_port}")
|
||||
if override_username:
|
||||
log.info(f"Using override mongodb username {override_username}")
|
||||
if override_password:
|
||||
log.info(f"Using override mongodb password ******")
|
||||
if override_query:
|
||||
log.info(f"Using override mongodb query {override_query}")
|
||||
|
||||
for key, alias in get_items(Database).items():
|
||||
if key not in db_entries:
|
||||
@@ -69,12 +81,20 @@ class DatabaseFactory:
|
||||
entry = cls._create_db_entry(alias=alias, settings=db_entries.get(key))
|
||||
|
||||
if override_connection_string:
|
||||
entry.host = override_connection_string
|
||||
con_str = f"{override_connection_string.rstrip('/')}/{key}"
|
||||
log.info(f"Using override mongodb connection string for {alias}: {con_str}")
|
||||
entry.host = con_str
|
||||
else:
|
||||
if override_hostname:
|
||||
entry.host = furl(entry.host).set(host=override_hostname).url
|
||||
if override_port:
|
||||
entry.host = furl(entry.host).set(port=override_port).url
|
||||
if override_username:
|
||||
entry.host = furl(entry.host).set(username=override_username).url
|
||||
if override_password:
|
||||
entry.host = furl(entry.host).set(password=override_password).url
|
||||
if override_query:
|
||||
entry.host = furl(entry.host).set(query=override_query).url
|
||||
|
||||
try:
|
||||
entry.validate()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -166,7 +166,10 @@ class MongoEngineErrorsHandler(object):
|
||||
@classmethod
|
||||
@throws_default_error(errors.server_error.InternalError)
|
||||
def invalid_query_error(cls, e, message, **_):
|
||||
pass
|
||||
if e.args:
|
||||
inner = e.args[0]
|
||||
if isinstance(inner, LookUpError):
|
||||
cls.lookup_error(inner, message)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -195,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)
|
||||
|
||||
@@ -60,3 +60,4 @@ def validate_id(cls, company, **kwargs):
|
||||
class EntityVisibility(Enum):
|
||||
active = "active"
|
||||
archived = "archived"
|
||||
hidden = "hidden"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, defaultdict
|
||||
from functools import reduce, partial
|
||||
from typing import (
|
||||
Collection,
|
||||
@@ -11,22 +11,23 @@ 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
|
||||
from apiserver.database.errors import MakeGetAllQueryError
|
||||
from apiserver.database.projection import project_dict, ProjectionHelper
|
||||
from apiserver.database.projection import ProjectionHelper
|
||||
from apiserver.database.props import PropsMixin
|
||||
from apiserver.database.query import RegexQ, RegexWrapper, RegexQCombination
|
||||
from apiserver.database.utils import (
|
||||
@@ -36,9 +37,10 @@ from apiserver.database.utils import (
|
||||
field_exists,
|
||||
)
|
||||
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"}
|
||||
|
||||
@@ -55,17 +57,25 @@ class ProperDictMixin(object):
|
||||
strip_private=True,
|
||||
only=None,
|
||||
extra_dict=None,
|
||||
exclude=None,
|
||||
) -> dict:
|
||||
return self.properize_dict(
|
||||
self.to_mongo(use_db_field=False).to_dict(),
|
||||
strip_private=strip_private,
|
||||
only=only,
|
||||
extra_dict=extra_dict,
|
||||
exclude=exclude,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def properize_dict(
|
||||
cls, d, strip_private=True, only=None, extra_dict=None, normalize_id=True
|
||||
cls,
|
||||
d,
|
||||
strip_private=True,
|
||||
only=None,
|
||||
extra_dict=None,
|
||||
exclude=None,
|
||||
normalize_id=True,
|
||||
):
|
||||
res = d
|
||||
if normalize_id and "_id" in res:
|
||||
@@ -76,6 +86,9 @@ class ProperDictMixin(object):
|
||||
res = project_dict(res, only)
|
||||
if extra_dict:
|
||||
res.update(extra_dict)
|
||||
if exclude:
|
||||
exclude_fields_from_dict(res, exclude)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -120,84 +133,131 @@ 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()
|
||||
|
||||
@@ -215,8 +275,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
|
||||
),
|
||||
)
|
||||
|
||||
@@ -335,84 +395,108 @@ 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()
|
||||
}
|
||||
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=parameters
|
||||
).items():
|
||||
query &= cls.get_list_field_query(field, 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, 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.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.fields or [], parameters=parameters
|
||||
).items():
|
||||
if "._" in field or "_." in field:
|
||||
query &= RegexQ(__raw__={field: data})
|
||||
else:
|
||||
dict_query[field.replace(".", "__")] = 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]
|
||||
for d in data: # type: str
|
||||
m = ACCESS_REGEX.match(d)
|
||||
if not m:
|
||||
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:
|
||||
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 = 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)
|
||||
|
||||
@@ -461,15 +545,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:
|
||||
@@ -497,6 +581,8 @@ class GetMixin(PropsMixin):
|
||||
def validate_order_by(cls, parameters, search_text) -> Sequence:
|
||||
"""
|
||||
Validate and extract order_by params as a list
|
||||
If ordering is specified then make sure that id field is part of it
|
||||
This guarantees the unique order when paging
|
||||
"""
|
||||
order_by = parameters.get(cls._ordering_key)
|
||||
if not order_by:
|
||||
@@ -509,6 +595,9 @@ class GetMixin(PropsMixin):
|
||||
"text score cannot be used in order_by when search text is not used"
|
||||
)
|
||||
|
||||
if not any(id_field in order_by for id_field in ("id", "-id")):
|
||||
order_by.append("id")
|
||||
|
||||
return order_by
|
||||
|
||||
@classmethod
|
||||
@@ -525,7 +614,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")
|
||||
@@ -648,6 +737,7 @@ class GetMixin(PropsMixin):
|
||||
allow_public=False,
|
||||
override_projection=None,
|
||||
expand_reference_ids=True,
|
||||
projection_fields: dict = None,
|
||||
ret_params: dict = None,
|
||||
):
|
||||
"""
|
||||
@@ -684,6 +774,7 @@ class GetMixin(PropsMixin):
|
||||
query=query,
|
||||
query_options=query_options,
|
||||
allow_public=allow_public,
|
||||
projection_fields=projection_fields,
|
||||
ret_params=ret_params,
|
||||
)
|
||||
|
||||
@@ -701,9 +792,50 @@ 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
|
||||
def get_count(
|
||||
cls: Union["GetMixin", Document],
|
||||
company,
|
||||
query_dict: dict = None,
|
||||
query_options: QueryParameterOptions = None,
|
||||
query: Q = None,
|
||||
allow_public=False,
|
||||
) -> int:
|
||||
_query = cls._get_combined_query(
|
||||
company=company,
|
||||
query_dict=query_dict,
|
||||
query_options=query_options,
|
||||
query=query,
|
||||
allow_public=allow_public,
|
||||
)
|
||||
return cls.objects(_query).count()
|
||||
|
||||
@classmethod
|
||||
def _get_combined_query(
|
||||
cls,
|
||||
company,
|
||||
query_dict: dict = None,
|
||||
query_options: QueryParameterOptions = None,
|
||||
query: Q = None,
|
||||
allow_public=False,
|
||||
) -> Q:
|
||||
if query_dict is not None:
|
||||
q = cls.prepare_query(
|
||||
parameters=query_dict,
|
||||
company=company,
|
||||
parameters_options=query_options,
|
||||
allow_public=allow_public,
|
||||
)
|
||||
else:
|
||||
q = cls._prepare_perm_query(company, allow_public=allow_public)
|
||||
|
||||
return (q & query) if query else q
|
||||
|
||||
@classmethod
|
||||
def get_many(
|
||||
cls,
|
||||
@@ -715,6 +847,7 @@ class GetMixin(PropsMixin):
|
||||
allow_public=False,
|
||||
override_projection: Collection[str] = None,
|
||||
return_dicts=True,
|
||||
projection_fields: dict = None,
|
||||
ret_params: dict = None,
|
||||
):
|
||||
"""
|
||||
@@ -749,16 +882,13 @@ class GetMixin(PropsMixin):
|
||||
if override_collation:
|
||||
break
|
||||
|
||||
if query_dict is not None:
|
||||
q = cls.prepare_query(
|
||||
parameters=query_dict,
|
||||
company=company,
|
||||
parameters_options=query_options,
|
||||
allow_public=allow_public,
|
||||
)
|
||||
else:
|
||||
q = cls._prepare_perm_query(company, allow_public=allow_public)
|
||||
_query = (q & query) if query else q
|
||||
_query = cls._get_combined_query(
|
||||
company=company,
|
||||
query_dict=query_dict,
|
||||
query_options=query_options,
|
||||
query=query,
|
||||
allow_public=allow_public,
|
||||
)
|
||||
|
||||
if return_dicts:
|
||||
data_getter = partial(
|
||||
@@ -767,6 +897,7 @@ class GetMixin(PropsMixin):
|
||||
parameters=parameters,
|
||||
override_projection=override_projection,
|
||||
override_collation=override_collation,
|
||||
projection_fields=projection_fields,
|
||||
)
|
||||
return cls.get_data_with_scroll_support(
|
||||
query_dict=query_dict, data_getter=data_getter, ret_params=ret_params,
|
||||
@@ -777,6 +908,7 @@ class GetMixin(PropsMixin):
|
||||
parameters=parameters,
|
||||
override_projection=override_projection,
|
||||
override_collation=override_collation,
|
||||
projection_fields=projection_fields,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -794,6 +926,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],
|
||||
@@ -801,6 +940,7 @@ class GetMixin(PropsMixin):
|
||||
parameters=None,
|
||||
override_projection=None,
|
||||
override_collation=None,
|
||||
projection_fields: dict = None,
|
||||
):
|
||||
"""
|
||||
Fetch all documents matching a provided query.
|
||||
@@ -834,7 +974,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
|
||||
@@ -843,6 +983,9 @@ class GetMixin(PropsMixin):
|
||||
if exclude:
|
||||
qs = qs.exclude(*exclude)
|
||||
|
||||
if projection_fields:
|
||||
qs = qs.fields(**projection_fields)
|
||||
|
||||
if start is not None and size:
|
||||
# add paging
|
||||
qs = qs.skip(start).limit(size)
|
||||
@@ -884,6 +1027,7 @@ class GetMixin(PropsMixin):
|
||||
parameters: dict = None,
|
||||
override_projection: Collection[str] = None,
|
||||
override_collation: dict = None,
|
||||
projection_fields: dict = None,
|
||||
) -> Sequence[dict]:
|
||||
"""
|
||||
Fetch all documents matching a provided query. For the first order by field
|
||||
@@ -922,7 +1066,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)
|
||||
|
||||
@@ -941,8 +1085,15 @@ class GetMixin(PropsMixin):
|
||||
if exclude:
|
||||
query_sets = [qs.exclude(*exclude) for qs in query_sets]
|
||||
|
||||
if projection_fields:
|
||||
query_sets = [qs.fields(**projection_fields) for qs in query_sets]
|
||||
|
||||
if start is None or not size:
|
||||
return [obj.to_proper_dict(only=include) for qs in query_sets for obj in qs]
|
||||
return [
|
||||
obj.to_proper_dict(only=include, exclude=exclude)
|
||||
for qs in query_sets
|
||||
for obj in qs
|
||||
]
|
||||
|
||||
# add paging
|
||||
ret = []
|
||||
@@ -950,7 +1101,7 @@ class GetMixin(PropsMixin):
|
||||
for i, qs in enumerate(query_sets):
|
||||
last_size = len(ret)
|
||||
ret.extend(
|
||||
obj.to_proper_dict(only=include)
|
||||
obj.to_proper_dict(only=include, exclude=exclude)
|
||||
for obj in (qs.skip(start) if start else qs).limit(size)
|
||||
)
|
||||
added = len(ret) - last_size
|
||||
@@ -1075,7 +1226,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)
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
@@ -36,6 +40,7 @@ class Model(AttributedDocument):
|
||||
("company", "framework"),
|
||||
("company", "name"),
|
||||
("company", "user"),
|
||||
("company", "uri"),
|
||||
{
|
||||
"name": "%s.model.main_text_index" % Database.backend,
|
||||
"fields": ["$name", "$id", "$comment", "$parent", "$task", "$project"],
|
||||
@@ -66,6 +71,7 @@ class Model(AttributedDocument):
|
||||
"parent",
|
||||
"metadata.*",
|
||||
),
|
||||
range_fields=("last_metrics.*", "last_iteration"),
|
||||
datetime_fields=("last_update",),
|
||||
)
|
||||
|
||||
@@ -91,3 +97,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 ""
|
||||
|
||||
@@ -9,7 +9,7 @@ from apiserver.database.model.base import GetMixin
|
||||
class Project(AttributedDocument):
|
||||
|
||||
get_all_query_options = GetMixin.QueryParameterOptions(
|
||||
pattern_fields=("name", "description"),
|
||||
pattern_fields=("name", "basename", "description"),
|
||||
list_fields=("tags", "system_tags", "id", "parent", "path"),
|
||||
range_fields=("last_update",),
|
||||
)
|
||||
@@ -21,6 +21,7 @@ class Project(AttributedDocument):
|
||||
"parent",
|
||||
"path",
|
||||
("company", "name"),
|
||||
("company", "basename"),
|
||||
{
|
||||
"name": "%s.project.main_text_index" % Database.backend,
|
||||
"fields": ["$name", "$id", "$description"],
|
||||
@@ -37,6 +38,7 @@ class Project(AttributedDocument):
|
||||
min_length=3,
|
||||
sparse=True,
|
||||
)
|
||||
basename = StrippedStringField(required=True)
|
||||
description = StringField()
|
||||
created = DateTimeField(required=True)
|
||||
tags = SafeSortedListField(StringField(required=True))
|
||||
|
||||
@@ -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"
|
||||
@@ -175,6 +179,8 @@ class Task(AttributedDocument):
|
||||
"active_duration",
|
||||
"parent",
|
||||
"project",
|
||||
"last_update",
|
||||
"status_changed",
|
||||
"models.input.model",
|
||||
("company", "name"),
|
||||
("company", "user"),
|
||||
@@ -193,6 +199,7 @@ class Task(AttributedDocument):
|
||||
"$name",
|
||||
"$id",
|
||||
"$comment",
|
||||
"$report",
|
||||
"$models.input.model",
|
||||
"$models.output.model",
|
||||
"$script.repository",
|
||||
@@ -203,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,
|
||||
@@ -225,7 +233,8 @@ class Task(AttributedDocument):
|
||||
),
|
||||
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=("execution.queue", "runtime.*", "models.input.model"),
|
||||
)
|
||||
|
||||
id = StringField(primary_key=True)
|
||||
@@ -239,6 +248,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()
|
||||
@@ -257,9 +268,10 @@ class Task(AttributedDocument):
|
||||
last_change = DateTimeField()
|
||||
last_iteration = IntField(default=DEFAULT_LAST_ITERATION)
|
||||
last_metrics = SafeMapField(field=SafeMapField(EmbeddedDocumentField(MetricEvent)))
|
||||
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)
|
||||
@@ -268,6 +280,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:
|
||||
"""
|
||||
|
||||
52
apiserver/database/model/url_to_delete.py
Normal file
52
apiserver/database/model/url_to_delete.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from enum import Enum
|
||||
|
||||
from mongoengine import StringField, DateTimeField, IntField, EnumField
|
||||
|
||||
from apiserver.database import Database, strict
|
||||
from apiserver.database.model import AttributedDocument
|
||||
|
||||
|
||||
class StorageType(str, Enum):
|
||||
fileserver = "fileserver"
|
||||
s3 = "s3"
|
||||
azure = "azure"
|
||||
gs = "gs"
|
||||
unknown = "unknown"
|
||||
|
||||
|
||||
class FileType(str, Enum):
|
||||
file = "file"
|
||||
folder = "folder"
|
||||
|
||||
|
||||
class DeletionStatus(str, Enum):
|
||||
created = "created"
|
||||
retrying = "retrying"
|
||||
failed = "failed"
|
||||
|
||||
|
||||
class UrlToDelete(AttributedDocument):
|
||||
_field_collation_overrides = {
|
||||
"url": AttributedDocument._numeric_locale,
|
||||
}
|
||||
|
||||
meta = {
|
||||
"db_alias": Database.backend,
|
||||
"strict": strict,
|
||||
"indexes": [
|
||||
("company", "user", "task"),
|
||||
("company", "storage_type", "url"),
|
||||
("status", "retry_count", "storage_type"),
|
||||
],
|
||||
}
|
||||
|
||||
id = StringField(primary_key=True)
|
||||
url = StringField(required=True, unique_with="company")
|
||||
task = StringField(required=True)
|
||||
created = DateTimeField(required=True)
|
||||
storage_type = EnumField(StorageType, default=StorageType.unknown)
|
||||
type = EnumField(FileType, default=FileType.file)
|
||||
retry_count = IntField(default=0)
|
||||
last_failure_time = DateTimeField()
|
||||
last_failure_reason = StringField()
|
||||
status = EnumField(DeletionStatus, default=DeletionStatus.created)
|
||||
@@ -1,4 +1,4 @@
|
||||
from mongoengine import Document, StringField, DynamicField
|
||||
from mongoengine import Document, StringField, DynamicField, DateTimeField
|
||||
|
||||
from apiserver.database import Database, strict
|
||||
from apiserver.database.model import DbModelMixin
|
||||
@@ -20,3 +20,4 @@ class User(DbModelMixin, Document):
|
||||
given_name = StringField(user_set_allowed=True)
|
||||
avatar = StringField()
|
||||
preferences = DynamicField(default="", exclude_by_default=True)
|
||||
created = DateTimeField()
|
||||
|
||||
@@ -1,73 +1,16 @@
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from itertools import groupby, chain
|
||||
from typing import Sequence, Dict, Callable, Tuple, Any, Type
|
||||
from typing import Sequence, Dict, Callable
|
||||
|
||||
import dpath.path
|
||||
from boltons import iterutils
|
||||
|
||||
from apiserver.apierrors import errors
|
||||
from apiserver.config_repo import config
|
||||
from apiserver.database.props import PropsMixin
|
||||
|
||||
SEP = "."
|
||||
|
||||
|
||||
def project_dict(data, projection, separator=SEP):
|
||||
"""
|
||||
Project partial data from a dictionary into a new dictionary
|
||||
:param data: Input dictionary
|
||||
:param projection: List of dictionary paths (each a string with field names separated using a separator)
|
||||
:param separator: Separator (default is '.')
|
||||
:return: A new dictionary containing only the projected parts from the original dictionary
|
||||
"""
|
||||
assert isinstance(data, dict)
|
||||
result = {}
|
||||
|
||||
def copy_path(path_parts, source, destination):
|
||||
src, dst = source, destination
|
||||
try:
|
||||
for depth, path_part in enumerate(path_parts[:-1]):
|
||||
src_part = src[path_part]
|
||||
if isinstance(src_part, dict):
|
||||
src = src_part
|
||||
dst = dst.setdefault(path_part, {})
|
||||
elif isinstance(src_part, (list, tuple)):
|
||||
if path_part not in dst:
|
||||
dst[path_part] = [{} for _ in range(len(src_part))]
|
||||
elif not isinstance(dst[path_part], (list, tuple)):
|
||||
raise TypeError(
|
||||
"Incompatible destination type %s for %s (list expected)"
|
||||
% (type(dst), separator.join(path_parts[: depth + 1]))
|
||||
)
|
||||
elif not len(dst[path_part]) == len(src_part):
|
||||
raise ValueError(
|
||||
"Destination list length differs from source length for %s"
|
||||
% separator.join(path_parts[: depth + 1])
|
||||
)
|
||||
|
||||
dst[path_part] = [
|
||||
copy_path(path_parts[depth + 1 :], s, d)
|
||||
for s, d in zip(src_part, dst[path_part])
|
||||
]
|
||||
|
||||
return destination
|
||||
else:
|
||||
raise TypeError(
|
||||
"Unsupported projection type %s for %s"
|
||||
% (type(src), separator.join(path_parts[: depth + 1]))
|
||||
)
|
||||
|
||||
last_part = path_parts[-1]
|
||||
dst[last_part] = src[last_part]
|
||||
except KeyError:
|
||||
# Projection field not in source, no biggie.
|
||||
pass
|
||||
return destination
|
||||
|
||||
for projection_path in sorted(projection):
|
||||
copy_path(
|
||||
path_parts=projection_path.split(separator), source=data, destination=result
|
||||
)
|
||||
return result
|
||||
max_items_per_fetch = config.get("services._mongo.max_page_size", 500)
|
||||
|
||||
|
||||
class _ReferenceProxy(dict):
|
||||
@@ -110,9 +53,6 @@ class ProjectionHelper(object):
|
||||
self._ref_projection = None
|
||||
self._proxy_manager = _ProxyManager()
|
||||
|
||||
# Cached dpath paths for each of the result documents
|
||||
self._cached_results_paths: Dict[int, Sequence[Tuple[Any, Type]]] = {}
|
||||
|
||||
self._parse_projection(projection)
|
||||
|
||||
def _collect_projection_fields(self, doc_cls, projection):
|
||||
@@ -275,25 +215,26 @@ class ProjectionHelper(object):
|
||||
norm_path = doc_cls.get_dpath_translated_path(path)
|
||||
globlist = norm_path.strip(SEP).split(SEP)
|
||||
|
||||
obj_paths = self._cached_results_paths.get(id(obj))
|
||||
if obj_paths is None:
|
||||
obj_paths = self._cached_results_paths[id(obj)] = list(
|
||||
dpath.path.paths(obj, dirs=True, skip=True)
|
||||
)
|
||||
|
||||
paths = [p for p in obj_paths if dpath.path.match(p, globlist)]
|
||||
|
||||
def search_and_replace(p: Sequence[Tuple[str, Type]]) -> Any:
|
||||
def _search_and_replace(target: dict, p: Sequence[str]) -> Sequence[str]:
|
||||
parent = None
|
||||
target = obj
|
||||
for part in p:
|
||||
parent = target
|
||||
target = target[part[0]]
|
||||
if parent and factory:
|
||||
parent[p[-1][0]] = factory(target)
|
||||
return target
|
||||
for idx, part in enumerate(p):
|
||||
if isinstance(target, dict) and part in target:
|
||||
parent = target
|
||||
target = target[part]
|
||||
elif isinstance(target, list) and part == "*":
|
||||
return list(
|
||||
chain.from_iterable(
|
||||
_search_and_replace(t, p[idx + 1 :]) for t in target
|
||||
)
|
||||
)
|
||||
else:
|
||||
return []
|
||||
|
||||
return [search_and_replace(p) for p in paths]
|
||||
if parent and factory:
|
||||
parent[p[-1]] = factory(target)
|
||||
return [target]
|
||||
|
||||
return _search_and_replace(obj, globlist)
|
||||
|
||||
def project(self, results, projection_func):
|
||||
"""
|
||||
@@ -341,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])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import hashlib
|
||||
from inspect import ismethod, getmembers
|
||||
from typing import Sequence, Tuple, Set, Optional, Callable, Any
|
||||
from typing import Sequence, Tuple, Set, Optional, Callable, Any, Mapping
|
||||
from uuid import uuid4
|
||||
|
||||
from mongoengine import EmbeddedDocumentField, ListField, Document, Q
|
||||
@@ -203,18 +203,22 @@ def _names_set(*names: str) -> Set[str]:
|
||||
return set(names) | set(f"-{name}" for name in names)
|
||||
|
||||
|
||||
system_tag_names = {
|
||||
_system_tag_names = {
|
||||
"model": _names_set("active", "archived"),
|
||||
"project": _names_set("archived", "public", "default"),
|
||||
"task": _names_set("active", "archived", "development"),
|
||||
"queue": _names_set("default"),
|
||||
}
|
||||
|
||||
system_tag_prefixes = {"task": _names_set("annotat")}
|
||||
_system_tag_prefixes = {"task": _names_set("annotat")}
|
||||
|
||||
|
||||
def partition_tags(
|
||||
entity: str, tags: Sequence[str], system_tags: Optional[Sequence[str]] = ()
|
||||
entity: str,
|
||||
tags: Sequence[str],
|
||||
system_tags: Optional[Sequence[str]] = (),
|
||||
system_tag_names: Mapping = _system_tag_names,
|
||||
system_tag_prefixes: Mapping = _system_tag_prefixes,
|
||||
) -> Tuple[Sequence[str], Sequence[str]]:
|
||||
"""
|
||||
Partition the given tags sequence into system and user-defined tags
|
||||
|
||||
@@ -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": {
|
||||
@@ -35,6 +35,12 @@
|
||||
},
|
||||
"value": {
|
||||
"type": "float"
|
||||
},
|
||||
"company_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"model_event": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
611
apiserver/jobs/async_urls_delete.py
Normal file
611
apiserver/jobs/async_urls_delete.py
Normal file
@@ -0,0 +1,611 @@
|
||||
import os
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from argparse import ArgumentParser
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
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, 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))
|
||||
storage_bll = StorageBLL()
|
||||
|
||||
|
||||
def mark_retry_failed(ids: Sequence[str], reason: str):
|
||||
UrlToDelete.objects(id__in=ids).update(
|
||||
last_failure_time=datetime.utcnow(),
|
||||
last_failure_reason=reason,
|
||||
inc__retry_count=1,
|
||||
)
|
||||
UrlToDelete.objects(id__in=ids, retry_count__gte=max_retries).update(
|
||||
status=DeletionStatus.failed
|
||||
)
|
||||
|
||||
|
||||
def mark_failed(query: Q, reason: str):
|
||||
UrlToDelete.objects(query).update(
|
||||
status=DeletionStatus.failed,
|
||||
last_failure_time=datetime.utcnow(),
|
||||
last_failure_reason=reason,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
url.url = url_path[len(path_prefix or "") :]
|
||||
return self.host
|
||||
|
||||
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:
|
||||
host = base
|
||||
session = requests.session()
|
||||
res = session.get(url=host, timeout=self.Client.timeout)
|
||||
res.raise_for_status()
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def run_delete_loop(fileserver_host: str):
|
||||
storage_helpers = {
|
||||
StorageType.fileserver: partial(
|
||||
FileserverStorage, fileserver_host=fileserver_host
|
||||
),
|
||||
StorageType.s3: AWSStorage,
|
||||
StorageType.azure: AzureStorage,
|
||||
StorageType.gs: GoogleCloudStorage,
|
||||
}
|
||||
while True:
|
||||
now = datetime.utcnow()
|
||||
urls_query = (
|
||||
Q(status__ne=DeletionStatus.failed)
|
||||
& Q(retry_count__lt=max_retries)
|
||||
& (
|
||||
Q(last_failure_time__exists=False)
|
||||
| Q(last_failure_time__lt=now - retry_timeout)
|
||||
)
|
||||
)
|
||||
|
||||
url_to_delete: UrlToDelete = UrlToDelete.objects(
|
||||
urls_query & Q(storage_type__in=list(storage_helpers))
|
||||
).order_by("retry_count").limit(1).first()
|
||||
if not url_to_delete:
|
||||
sleep(10)
|
||||
continue
|
||||
|
||||
company = url_to_delete.company
|
||||
user = url_to_delete.user
|
||||
storage_type = url_to_delete.storage_type
|
||||
log.info(
|
||||
f"Deleting {storage_type} objects for company: {company}, user: {user}"
|
||||
)
|
||||
company_storage_urls_query = urls_query & Q(
|
||||
company=company, storage_type=storage_type,
|
||||
)
|
||||
delete_urls(
|
||||
urls_query=company_storage_urls_query,
|
||||
storage=storage_helpers[storage_type](company=company),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description=__doc__)
|
||||
|
||||
parser.add_argument(
|
||||
"--fileserver-host", "-fh", help="Fileserver host address", type=str,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
db.initialize()
|
||||
run_delete_loop(args.fileserver_host)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,10 @@
|
||||
import importlib.util
|
||||
from datetime import datetime
|
||||
from inspect import signature
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
|
||||
import pymongo.database
|
||||
from mongoengine.connection import get_db
|
||||
from packaging.version import Version, parse
|
||||
|
||||
@@ -16,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:
|
||||
@@ -80,8 +88,15 @@ def _apply_migrations(log: Logger):
|
||||
if not func:
|
||||
continue
|
||||
try:
|
||||
sig = signature(func)
|
||||
kwargs = {}
|
||||
if len(sig.parameters) == 2:
|
||||
name, param = list(sig.parameters.items())[-1]
|
||||
key = name.replace("_", "-")
|
||||
if issubclass(param.annotation, pymongo.database.Database) and key in dbs:
|
||||
kwargs[name] = get_db(key)
|
||||
log.info(f"Applying {script.stem}/{func_name}()")
|
||||
func(get_db(alias))
|
||||
func(get_db(alias), **kwargs)
|
||||
except Exception:
|
||||
log.exception(f"Failed applying {script}:{func_name}()")
|
||||
raise ValueError(
|
||||
|
||||
@@ -24,6 +24,7 @@ from typing import (
|
||||
Callable,
|
||||
)
|
||||
from urllib.parse import unquote, urlparse
|
||||
from uuid import uuid4, UUID, uuid5
|
||||
from zipfile import ZipFile, ZIP_BZIP2
|
||||
|
||||
import mongoengine
|
||||
@@ -71,6 +72,8 @@ class PrePopulate:
|
||||
r"['\"]source['\"]:\s?['\"](https?://(?:localhost:8081|files.*?)/.*?)['\"]",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
_name_guid_ns = UUID("bda3acc1-e612-506c-bade-80071b6cf039")
|
||||
_example_id_prefix = "e-"
|
||||
task_cls: Type[Task]
|
||||
project_cls: Type[Project]
|
||||
model_cls: Type[Model]
|
||||
@@ -690,6 +693,58 @@ class PrePopulate:
|
||||
continue
|
||||
yield clean
|
||||
|
||||
@staticmethod
|
||||
def _new_id(_):
|
||||
return str(uuid4()).replace("-", "")
|
||||
|
||||
@classmethod
|
||||
def _hash_id(cls, name: str):
|
||||
return str(uuid5(cls._name_guid_ns, name)).replace("-", "")
|
||||
|
||||
@classmethod
|
||||
def _example_id(cls, orig_id: str):
|
||||
if not orig_id or orig_id.startswith(cls._example_id_prefix):
|
||||
return orig_id
|
||||
|
||||
return cls._example_id_prefix + orig_id
|
||||
|
||||
@classmethod
|
||||
def _private_id(cls, orig_id: str):
|
||||
if not orig_id or not orig_id.startswith(cls._example_id_prefix):
|
||||
return orig_id
|
||||
|
||||
return orig_id[len(cls._example_id_prefix) :]
|
||||
|
||||
@classmethod
|
||||
def _generate_new_ids(
|
||||
cls, reader: ZipFile, entity_files: Sequence, metadata: Mapping[str, Any],
|
||||
) -> Mapping[str, str]:
|
||||
if not metadata or not any(
|
||||
metadata.get(key) for key in ("new_ids", "example_ids", "private_ids")
|
||||
):
|
||||
return {}
|
||||
|
||||
ids = {}
|
||||
for entity_file in entity_files:
|
||||
with reader.open(entity_file) as f:
|
||||
is_project = splitext(entity_file.orig_filename)[0].endswith(".Project")
|
||||
if metadata.get("new_ids"):
|
||||
id_func = cls._new_id
|
||||
elif metadata.get("example_ids"):
|
||||
id_func = cls._example_id if not is_project else cls._hash_id
|
||||
elif metadata.get("private_ids"):
|
||||
id_func = cls._private_id if not is_project else cls._new_id
|
||||
for item in cls.json_lines(f):
|
||||
doc = json.loads(item)
|
||||
orig_id = doc.get("_id")
|
||||
if orig_id:
|
||||
ids[orig_id] = (
|
||||
id_func(orig_id)
|
||||
if id_func != cls._hash_id
|
||||
else id_func(doc.get("name"))
|
||||
)
|
||||
return ids
|
||||
|
||||
@classmethod
|
||||
def _import(
|
||||
cls,
|
||||
@@ -704,37 +759,42 @@ class PrePopulate:
|
||||
Start from entities since event import will require the tasks already in DB
|
||||
"""
|
||||
event_file_ending = cls.events_file_suffix + ".json"
|
||||
entity_files = (
|
||||
entity_files = [
|
||||
fi
|
||||
for fi in reader.filelist
|
||||
if not fi.orig_filename.endswith(event_file_ending)
|
||||
and fi.orig_filename != cls.metadata_filename
|
||||
)
|
||||
]
|
||||
metadata = metadata or {}
|
||||
old_to_new_ids = cls._generate_new_ids(reader, entity_files, metadata)
|
||||
tasks = []
|
||||
for entity_file in entity_files:
|
||||
with reader.open(entity_file) as f:
|
||||
full_name = splitext(entity_file.orig_filename)[0]
|
||||
print(f"Reading {reader.filename}:{full_name}...")
|
||||
res = cls._import_entity(f, full_name, company_id, user_id, metadata)
|
||||
res = cls._import_entity(
|
||||
f, full_name, company_id, user_id, metadata, old_to_new_ids
|
||||
)
|
||||
if res:
|
||||
tasks = res
|
||||
|
||||
if sort_tasks_by_last_updated:
|
||||
tasks = sorted(tasks, key=attrgetter("last_update"))
|
||||
|
||||
new_to_old_ids = {v: k for k, v in old_to_new_ids.items()}
|
||||
for task in tasks:
|
||||
old_task_id = new_to_old_ids.get(task.id, task.id)
|
||||
events_file = first(
|
||||
fi
|
||||
for fi in reader.filelist
|
||||
if fi.orig_filename.endswith(task.id + event_file_ending)
|
||||
if fi.orig_filename.endswith(old_task_id + event_file_ending)
|
||||
)
|
||||
if not events_file:
|
||||
continue
|
||||
with reader.open(events_file) as f:
|
||||
full_name = splitext(events_file.orig_filename)[0]
|
||||
print(f"Reading {reader.filename}:{full_name}...")
|
||||
cls._import_events(f, full_name, company_id, user_id)
|
||||
cls._import_events(f, company_id, user_id, task.id)
|
||||
|
||||
@classmethod
|
||||
def _get_entity_type(cls, full_name) -> Type[mongoengine.Document]:
|
||||
@@ -746,6 +806,15 @@ class PrePopulate:
|
||||
module = importlib.import_module(module_name)
|
||||
return getattr(module, class_name)
|
||||
|
||||
@staticmethod
|
||||
def _upgrade_project_data(project_data: dict) -> dict:
|
||||
if not project_data.get("basename"):
|
||||
name: str = project_data["name"]
|
||||
_, _, basename = name.rpartition("/")
|
||||
project_data["basename"] = basename
|
||||
|
||||
return project_data
|
||||
|
||||
@staticmethod
|
||||
def _upgrade_model_data(model_data: dict) -> dict:
|
||||
metadata_key = "metadata"
|
||||
@@ -838,6 +907,7 @@ class PrePopulate:
|
||||
company_id: str,
|
||||
user_id: str,
|
||||
metadata: Mapping[str, Any],
|
||||
old_to_new_ids: Mapping[str, str] = None,
|
||||
) -> Optional[Sequence[Task]]:
|
||||
cls_ = cls._get_entity_type(full_name)
|
||||
print(f"Writing {cls_.__name__.lower()}s into database")
|
||||
@@ -846,8 +916,14 @@ class PrePopulate:
|
||||
data_upgrade_funcs: Mapping[Type, Callable] = {
|
||||
cls.task_cls: cls._upgrade_task_data,
|
||||
cls.model_cls: cls._upgrade_model_data,
|
||||
cls.project_cls: cls._upgrade_project_data,
|
||||
}
|
||||
for item in cls.json_lines(f):
|
||||
if old_to_new_ids:
|
||||
for old_id, new_id in old_to_new_ids.items():
|
||||
# replace ids only when they are standalone strings
|
||||
# otherwise artifacts uris that contain old ids may get damaged
|
||||
item = item.replace(f'"{old_id}"', f'"{new_id}"')
|
||||
upgrade_func = data_upgrade_funcs.get(cls_)
|
||||
if upgrade_func:
|
||||
item = json.dumps(upgrade_func(json.loads(item)))
|
||||
@@ -884,11 +960,14 @@ class PrePopulate:
|
||||
return tasks
|
||||
|
||||
@classmethod
|
||||
def _import_events(cls, f: IO[bytes], full_name: str, company_id: str, _):
|
||||
_, _, task_id = full_name[0 : -len(cls.events_file_suffix)].rpartition("_")
|
||||
def _import_events(cls, f: IO[bytes], company_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_tasks=True
|
||||
company_id, events=events, worker=""
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ def _ensure_backend_user(user_id: str, company_id: str, user_name: str):
|
||||
name=user_name,
|
||||
given_name=given_name,
|
||||
family_name=family_name,
|
||||
created=datetime.utcnow(),
|
||||
).save()
|
||||
|
||||
return user_id
|
||||
|
||||
12
apiserver/mongo/migrations/1_6_0.py
Normal file
12
apiserver/mongo/migrations/1_6_0.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pymongo.collection import Collection
|
||||
from pymongo.database import Database
|
||||
|
||||
|
||||
def migrate_backend(db: Database):
|
||||
projects: Collection = db["project"]
|
||||
for doc in projects.find({"basename": None}):
|
||||
name: str = doc["name"]
|
||||
_, _, basename = name.rpartition("/")
|
||||
projects.update_one(
|
||||
{"_id": doc["_id"]}, {"$set": {"basename": basename}},
|
||||
)
|
||||
15
apiserver/mongo/migrations/1_7_0.py
Normal file
15
apiserver/mongo/migrations/1_7_0.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pymongo.collection import Collection
|
||||
from pymongo.database import Database
|
||||
|
||||
|
||||
def migrate_backend(db: Database, auth_db: Database):
|
||||
users: Collection = db["user"]
|
||||
auth_users: Collection = auth_db["user"]
|
||||
created_field = "created"
|
||||
for doc in users.find({created_field: {"$exists": False}}):
|
||||
auth_user = auth_users.find_one({"_id": doc["_id"]}, projection=[created_field])
|
||||
if not auth_user or created_field not in auth_user:
|
||||
continue
|
||||
users.update_one(
|
||||
{"_id": doc["_id"]}, {"$set": {created_field: auth_user[created_field]}}
|
||||
)
|
||||
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,34 +1,36 @@
|
||||
attrs>=19.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
|
||||
pyjwt<2.0.0
|
||||
pymongo[srv]==3.12.0
|
||||
pyhocon>=0.3.35r
|
||||
pyjwt>=2.4.0
|
||||
pymongo==4.4.0
|
||||
python-rapidjson>=0.6.3
|
||||
redis==3.5.3
|
||||
redis-py-cluster>=2.1.3
|
||||
related>=0.7.2
|
||||
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
|
||||
urllib3>=1.26.16
|
||||
|
||||
@@ -15,6 +15,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, 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: "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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,6 +262,38 @@ get_credentials {
|
||||
}
|
||||
}
|
||||
|
||||
edit_credentials {
|
||||
allow_roles = [ "*" ]
|
||||
internal: false
|
||||
"2.19" {
|
||||
description: """Updates the label of the existing credentials for the authenticated user."""
|
||||
request {
|
||||
type: object
|
||||
required: [ access_key ]
|
||||
properties {
|
||||
access_key {
|
||||
type: string
|
||||
description: Existing credentials key
|
||||
}
|
||||
label {
|
||||
type: string
|
||||
description: New credentials label
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
updated {
|
||||
description: "Number of credentials updated"
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revoke_credentials {
|
||||
allow_roles = [ "*" ]
|
||||
internal: false
|
||||
|
||||
@@ -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
|
||||
@@ -164,14 +153,6 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
scalar_key_enum {
|
||||
type: string
|
||||
enum: [
|
||||
iter
|
||||
timestamp
|
||||
iso_time
|
||||
]
|
||||
}
|
||||
log_level_enum {
|
||||
type: string
|
||||
enum: [
|
||||
@@ -260,48 +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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_image_sample_response {
|
||||
type: object
|
||||
properties {
|
||||
@@ -323,6 +262,28 @@ _definitions {
|
||||
}
|
||||
}
|
||||
}
|
||||
plot_sample_response {
|
||||
type: object
|
||||
properties {
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID to pass to the next calls to get_plot_sample or next_plot_sample"
|
||||
}
|
||||
events {
|
||||
description: "Plot events"
|
||||
type: array
|
||||
items { type: object}
|
||||
}
|
||||
min_iteration {
|
||||
type: integer
|
||||
description: "minimal valid iteration for the metric"
|
||||
}
|
||||
max_iteration {
|
||||
type: integer
|
||||
description: "maximal valid iteration for the metric"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add {
|
||||
"2.1" {
|
||||
@@ -342,13 +303,27 @@ add {
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
"2.22": ${add."2.1"} {
|
||||
request.properties {
|
||||
model_event {
|
||||
type: boolean
|
||||
description: If set then the event is for a model. Otherwise for a task. Cannot be used with task log events. If used in batch then all the events should be marked the same
|
||||
default: false
|
||||
}
|
||||
allow_locked {
|
||||
type: boolean
|
||||
description: Allow adding events to published tasks or models
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add_batch {
|
||||
"2.1" {
|
||||
description: "Adds a batch of events in a single call (json-lines format, stream-friendly)"
|
||||
batch_request: {
|
||||
action: add
|
||||
version: 1.5
|
||||
version: 2.1
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
@@ -359,10 +334,16 @@ add_batch {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${add_batch."2.1"} {
|
||||
batch_request: {
|
||||
action: add
|
||||
version: 2.22
|
||||
}
|
||||
}
|
||||
}
|
||||
delete_for_task {
|
||||
"2.1" {
|
||||
description: "Delete all task event. *This cannot be undone!*"
|
||||
description: "Delete all task events. *This cannot be undone!*"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
@@ -391,6 +372,37 @@ delete_for_task {
|
||||
}
|
||||
}
|
||||
}
|
||||
delete_for_model {
|
||||
"2.22" {
|
||||
description: "Delete all model events. *This cannot be undone!*"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
model
|
||||
]
|
||||
properties {
|
||||
model {
|
||||
type: string
|
||||
description: "Model ID"
|
||||
}
|
||||
allow_locked {
|
||||
type: boolean
|
||||
description: "Allow deleting events even if the model is locked"
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
deleted {
|
||||
type: boolean
|
||||
description: "Number of deleted events"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_images {
|
||||
"2.1" {
|
||||
description: "Get all debug images of a task"
|
||||
@@ -432,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
|
||||
@@ -485,6 +497,55 @@ debug_images {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${debug_images."2.14"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
plots {
|
||||
"2.20" {
|
||||
description: "Get plot events for the requested amount of iterations per each task"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
metrics
|
||||
]
|
||||
properties {
|
||||
metrics {
|
||||
type: array
|
||||
description: List of metrics and variants
|
||||
items { "$ref": "#/definitions/task_metric_variants" }
|
||||
}
|
||||
iters {
|
||||
type: integer
|
||||
description: "Max number of latest iterations for which to return plots"
|
||||
}
|
||||
navigate_earlier {
|
||||
type: boolean
|
||||
description: "If set then events are retreived from latest iterations to earliest ones. Otherwise from earliest iterations to the latest. The default is True"
|
||||
}
|
||||
refresh {
|
||||
type: boolean
|
||||
description: "If set then scroll will be moved to the latest iterations. The default is False"
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID of previous call (used for getting more results)"
|
||||
}
|
||||
}
|
||||
}
|
||||
response {"$ref": "#/definitions/plots_response"}
|
||||
}
|
||||
"2.22": ${plots."2.20"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model plots. Otherwise task plots
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_debug_image_sample {
|
||||
"2.12": {
|
||||
@@ -521,6 +582,20 @@ get_debug_image_sample {
|
||||
}
|
||||
response {"$ref": "#/definitions/debug_image_sample_response"}
|
||||
}
|
||||
"2.20": ${get_debug_image_sample."2.12"} {
|
||||
request.properties.navigate_current_metric {
|
||||
description: If set then subsequent navigation with next_debug_image_sample is done on the debug images for the passed metric only. Otherwise for all the metrics
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
"2.22": ${get_debug_image_sample."2.20"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model debug images. Otherwise task debug images
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
next_debug_image_sample {
|
||||
"2.12": {
|
||||
@@ -546,6 +621,99 @@ next_debug_image_sample {
|
||||
}
|
||||
response {"$ref": "#/definitions/debug_image_sample_response"}
|
||||
}
|
||||
"2.22": ${next_debug_image_sample."2.12"} {
|
||||
request.properties.next_iteration {
|
||||
type: boolean
|
||||
default: false
|
||||
description: If set then navigate to the next/previous iteration
|
||||
}
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model debug images. Otherwise task debug images
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_plot_sample {
|
||||
"2.20": {
|
||||
description: "Return plots for the provided iteration"
|
||||
request {
|
||||
type: object
|
||||
required: [task, metric]
|
||||
properties {
|
||||
task {
|
||||
description: "Task ID"
|
||||
type: string
|
||||
}
|
||||
metric {
|
||||
description: "Metric name"
|
||||
type: string
|
||||
}
|
||||
iteration {
|
||||
description: "The iteration to bring plot from. If not specified then the latest reported plot is retrieved"
|
||||
type: integer
|
||||
}
|
||||
refresh {
|
||||
description: "If set then scroll state will be refreshed to reflect the latest changes in the plots"
|
||||
type: boolean
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID from the previous call to get_plot_sample or empty"
|
||||
}
|
||||
navigate_current_metric {
|
||||
description: If set then subsequent navigation with next_plot_sample is done on the plots for the passed metric only. Otherwise for all the metrics
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
response {"$ref": "#/definitions/plot_sample_response"}
|
||||
}
|
||||
"2.22": ${get_plot_sample."2.20"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model plots. Otherwise task plots
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
next_plot_sample {
|
||||
"2.20": {
|
||||
description: "Get the plot for the next metric for the same iteration or for the next iteration"
|
||||
request {
|
||||
type: object
|
||||
required: [task, scroll_id]
|
||||
properties {
|
||||
task {
|
||||
description: "Task ID"
|
||||
type: string
|
||||
}
|
||||
scroll_id {
|
||||
type: string
|
||||
description: "Scroll ID from the previous call to get_plot_sample"
|
||||
}
|
||||
navigate_earlier {
|
||||
type: boolean
|
||||
description: """If set then get the either previous metric events from the current iteration or (if does not exist) the last metric events from the previous iteration.
|
||||
Otherwise next metric events from the current iteration or first metric events from the next iteration"""
|
||||
}
|
||||
}
|
||||
}
|
||||
response {"$ref": "#/definitions/plot_sample_response"}
|
||||
}
|
||||
"2.22": ${next_plot_sample."2.20"} {
|
||||
request.properties.next_iteration {
|
||||
type: boolean
|
||||
default: false
|
||||
description: If set then navigate to the next/previous iteration
|
||||
}
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model plots. Otherwise task plots
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_task_metrics{
|
||||
"2.7": {
|
||||
@@ -578,6 +746,13 @@ get_task_metrics{
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${get_task_metrics."2.7"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then get metrics from model events. Otherwise from task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_task_log {
|
||||
"1.5" {
|
||||
@@ -677,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
|
||||
@@ -731,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -786,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
|
||||
@@ -795,6 +970,13 @@ get_task_events {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${get_task_events."2.1"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then get retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
download_task_log {
|
||||
@@ -845,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
|
||||
@@ -869,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
|
||||
@@ -896,6 +1078,13 @@ get_task_plots {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.22": ${get_task_plots."2.16"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_multi_task_plots {
|
||||
"2.1" {
|
||||
@@ -916,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
|
||||
@@ -937,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
|
||||
@@ -953,6 +1142,20 @@ get_multi_task_plots {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.22": ${get_multi_task_plots."2.16"} {
|
||||
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" {
|
||||
@@ -981,6 +1184,13 @@ get_vector_metrics_and_variants {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${get_vector_metrics_and_variants."2.1"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
vector_metrics_iter_histogram {
|
||||
"2.1" {
|
||||
@@ -1019,6 +1229,13 @@ vector_metrics_iter_histogram {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${vector_metrics_iter_histogram."2.1"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
scalar_metrics_iter_histogram {
|
||||
"2.1" {
|
||||
@@ -1072,6 +1289,13 @@ scalar_metrics_iter_histogram {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${scalar_metrics_iter_histogram."2.14"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
multi_task_scalar_metrics_iter_histogram {
|
||||
"2.1" {
|
||||
@@ -1083,11 +1307,11 @@ 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
|
||||
description: "List of task Task IDs"
|
||||
description: "Task ID"
|
||||
}
|
||||
}
|
||||
samples {
|
||||
@@ -1111,6 +1335,40 @@ multi_task_scalar_metrics_iter_histogram {
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
"2.22": ${multi_task_scalar_metrics_iter_histogram."2.1"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_task_single_value_metrics {
|
||||
"2.20" {
|
||||
description: Get single value metrics for the passed tasks
|
||||
request {
|
||||
type: object
|
||||
required: [tasks]
|
||||
properties {
|
||||
tasks {
|
||||
description: "List of task Task IDs"
|
||||
type: array
|
||||
items {
|
||||
type: string
|
||||
description: "Task ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
response {"$ref": "#/definitions/single_value_metrics_response"}
|
||||
}
|
||||
"2.22": ${get_task_single_value_metrics."2.20"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_task_latest_scalar_values {
|
||||
"2.1" {
|
||||
@@ -1190,6 +1448,13 @@ get_scalar_metrics_and_variants {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${get_scalar_metrics_and_variants."2.1"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_scalar_metric_data {
|
||||
"2.1" {
|
||||
@@ -1239,6 +1504,13 @@ get_scalar_metric_data {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.22": ${get_scalar_metric_data."2.16"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
scalar_metrics_iter_raw {
|
||||
"2.16" {
|
||||
@@ -1303,4 +1575,66 @@ scalar_metrics_iter_raw {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${scalar_metrics_iter_raw."2.16"} {
|
||||
request.properties.model_events {
|
||||
type: boolean
|
||||
description: If set then the retrieving model events. Otherwise task events
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
clear_scroll {
|
||||
"2.18" {
|
||||
description: "Clear an open Scroll ID"
|
||||
request {
|
||||
type: object
|
||||
required: [
|
||||
scroll_id
|
||||
]
|
||||
properties {
|
||||
scroll_id {
|
||||
description: "Scroll ID as returned by previous events service calls"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear_task_log {
|
||||
"2.19" {
|
||||
description: Remove old logs from task
|
||||
request {
|
||||
type: object
|
||||
required: [task]
|
||||
properties {
|
||||
task {
|
||||
description: Task ID
|
||||
type: string
|
||||
}
|
||||
allow_locked {
|
||||
type: boolean
|
||||
description: Allow deleting events even if the task is locked
|
||||
default: false
|
||||
}
|
||||
threshold_sec {
|
||||
description: The amount of seconds ago to retain the log records. The older log records will be deleted. If not passed or 0 then all the log records for the task will be deleted
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
deleted {
|
||||
description: The number of deleted log records
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,27 @@ _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
|
||||
properties {
|
||||
labels_count {
|
||||
description: Number of the model labels
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
published_task_item {
|
||||
@@ -224,6 +245,22 @@ get_all_ex {
|
||||
description: "Scroll ID that can be used with the next calls to get_all_ex to retrieve more data"
|
||||
}
|
||||
}
|
||||
"2.20": ${get_all_ex."2.15"} {
|
||||
request.properties.include_stats {
|
||||
description: "If true, include models statistic in response"
|
||||
type: boolean
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all {
|
||||
"2.1" {
|
||||
@@ -303,6 +340,14 @@ get_all {
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
last_update {
|
||||
description: "List of last_update constraint strings (utcformat, epoch) with an optional prefix modifier (>, >=, <, <=)"
|
||||
type: array
|
||||
items {
|
||||
type: string
|
||||
pattern: "^(>=|>|<=|<)?.*$"
|
||||
}
|
||||
}
|
||||
_all_ {
|
||||
description: "Multi-field pattern condition (all fields match pattern)"
|
||||
"$ref": "#/definitions/multi_field_pattern_data"
|
||||
@@ -350,6 +395,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" {
|
||||
@@ -951,7 +1007,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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -102,4 +135,164 @@ get_user_companies {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_entities_count {
|
||||
"2.20": {
|
||||
description: "Get counts for the company entities according to the passed search criteria"
|
||||
request {
|
||||
type: object
|
||||
properties {
|
||||
projects {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Search criteria for projects
|
||||
}
|
||||
tasks {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Search criteria for experiments
|
||||
}
|
||||
models {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Search criteria for models
|
||||
}
|
||||
pipelines {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Search criteria for pipelines
|
||||
}
|
||||
datasets {
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Search criteria for datasets
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
projects {
|
||||
type: integer
|
||||
description: The number of projects matching the criteria
|
||||
}
|
||||
tasks {
|
||||
type: integer
|
||||
description: The number of experiments matching the criteria
|
||||
}
|
||||
models {
|
||||
type: integer
|
||||
description: The number of models matching the criteria
|
||||
}
|
||||
pipelines {
|
||||
type: integer
|
||||
description: The number of pipelines matching the criteria
|
||||
}
|
||||
datasets {
|
||||
type: integer
|
||||
description: The number of datasets matching the criteria
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.22": ${get_entities_count."2.20"} {
|
||||
request.properties {
|
||||
search_hidden {
|
||||
description: "If set to 'true' then hidden projects and tasks are included in the search results"
|
||||
type: boolean
|
||||
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"
|
||||
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"
|
||||
|
||||
@@ -25,6 +25,10 @@ _definitions {
|
||||
description: "Project name"
|
||||
type: string
|
||||
}
|
||||
basename {
|
||||
description: "Project base name"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "Project description"
|
||||
type: string
|
||||
@@ -42,11 +46,6 @@ _definitions {
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_update {
|
||||
description: "Last update time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags"
|
||||
type: array
|
||||
@@ -62,12 +61,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 {
|
||||
@@ -142,6 +155,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 {
|
||||
@@ -156,6 +173,10 @@ _definitions {
|
||||
description: "Project name"
|
||||
type: string
|
||||
}
|
||||
basename {
|
||||
description: "Project base name"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "Project description"
|
||||
type: string
|
||||
@@ -173,11 +194,6 @@ _definitions {
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
last_update {
|
||||
description: "Last update time"
|
||||
type: string
|
||||
format: "date-time"
|
||||
}
|
||||
tags {
|
||||
description: "User-defined tags"
|
||||
type: array
|
||||
@@ -192,7 +208,16 @@ _definitions {
|
||||
description: "The default output destination URL for new tasks under this project"
|
||||
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"
|
||||
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"
|
||||
@@ -214,6 +239,32 @@ _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
|
||||
}
|
||||
own_models {
|
||||
description: "The amount of models under this project (without children projects). Returned if 'check_own_contents' flag is set in the request"
|
||||
type: integer
|
||||
}
|
||||
dataset_stats {
|
||||
description: Project dataset statistics
|
||||
type: object
|
||||
properties {
|
||||
file_count {
|
||||
type: integer
|
||||
description: The number of files stored in the dataset
|
||||
}
|
||||
total_size {
|
||||
type: integer
|
||||
description: The total dataset size in bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metric_variant_result {
|
||||
@@ -385,6 +436,10 @@ get_all {
|
||||
description: "Get only projects whose name matches this pattern (python regular expression syntax)"
|
||||
type: string
|
||||
}
|
||||
basename {
|
||||
description: "Project base name"
|
||||
type: string
|
||||
}
|
||||
description {
|
||||
description: "Get only projects whose description matches this pattern (python regular expression syntax)"
|
||||
type: string
|
||||
@@ -455,7 +510,14 @@ get_all {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all."2.13"} {
|
||||
"2.14": ${get_all."2.13"} {
|
||||
request.properties.search_hidden {
|
||||
description: "If set to 'true' then hidden projects are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all."2.14"} {
|
||||
request {
|
||||
properties {
|
||||
scroll_id {
|
||||
@@ -523,20 +585,15 @@ get_all_ex {
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
properties {
|
||||
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
|
||||
}
|
||||
own_models {
|
||||
description: "The amount of models under this project (without children projects). Returned if 'check_own_contents' flag is set in the request"
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.14": ${get_all_ex."2.13"} {
|
||||
request.properties.search_hidden {
|
||||
description: "If set to 'true' then hidden projects are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all_ex."2.13"} {
|
||||
"2.15": ${get_all_ex."2.14"} {
|
||||
request {
|
||||
properties {
|
||||
scroll_id {
|
||||
@@ -568,17 +625,41 @@ get_all_ex {
|
||||
}
|
||||
"2.17": ${get_all_ex."2.16"} {
|
||||
request.properties.include_stats_filter {
|
||||
description: The filter for selecting entities that participate in statistics calculation
|
||||
description: The filter for selecting entities that participate in statistics calculation. For each task field that you want to filter on pass the list of allowed values. Prepend the value with '-' to exclude
|
||||
type: object
|
||||
properties {
|
||||
system_tags {
|
||||
description: The list of allowed system tags
|
||||
type: array
|
||||
items { type: string }
|
||||
}
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
"2.20": ${get_all_ex."2.17"} {
|
||||
request.properties.include_dataset_stats {
|
||||
description: "If true, include project dataset statistic in response"
|
||||
type: boolean
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
update {
|
||||
"2.1" {
|
||||
@@ -727,6 +808,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" {
|
||||
@@ -787,6 +888,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" {
|
||||
@@ -824,6 +932,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" {
|
||||
@@ -871,6 +986,20 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_hyper_parameters {
|
||||
"2.9" {
|
||||
@@ -968,6 +1097,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" {
|
||||
@@ -1153,4 +1296,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,12 @@ get_by_id {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.20": ${get_by_id."2.4"} {
|
||||
request.properties.max_task_entries {
|
||||
description: Max number of queue task entries to return
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
// typescript generation hack
|
||||
get_all_ex {
|
||||
@@ -140,6 +146,19 @@ get_all_ex {
|
||||
description: "Scroll ID that can be used with the next calls to get_all_ex to retrieve more data"
|
||||
}
|
||||
}
|
||||
"2.20": ${get_all_ex."2.15"} {
|
||||
request.properties.max_task_entries {
|
||||
description: Max number of queue task entries to return
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
"2.21": ${get_all_ex."2.20"} {
|
||||
request.properties.search_hidden {
|
||||
description: "If set to 'true' then hidden queues are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all {
|
||||
"2.4" {
|
||||
@@ -226,6 +245,19 @@ get_all {
|
||||
description: "Scroll ID that can be used with the next calls to get_all to retrieve more data"
|
||||
}
|
||||
}
|
||||
"2.20": ${get_all."2.15"} {
|
||||
request.properties.max_task_entries {
|
||||
description: Max number of queue task entries to return
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
"2.21": ${get_all."2.20"} {
|
||||
request.properties.search_hidden {
|
||||
description: "If set to 'true' then hidden queues are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
get_default {
|
||||
"2.4" {
|
||||
@@ -431,6 +463,33 @@ get_next_task {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.14": ${get_next_task."2.4"} {
|
||||
request.properties.get_task_info {
|
||||
description: "If set then additional task info is returned"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
response.properties.task_info {
|
||||
description: "Info about the returned task. Returned only if get_task_info is set to True"
|
||||
type: object
|
||||
properties {
|
||||
company {
|
||||
description: Task company ID
|
||||
type: string
|
||||
}
|
||||
user {
|
||||
description: ID of the user who created the task
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.21": ${get_next_task."2.14"} {
|
||||
request.properties.task {
|
||||
description: Task company ID
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
remove_task {
|
||||
"2.4" {
|
||||
@@ -634,6 +693,13 @@ get_queue_metrics : {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.20": ${get_queue_metrics."2.4"} {
|
||||
request.properties.refresh {
|
||||
type: boolean
|
||||
default: false
|
||||
description: If set then the new queue metrics is taken
|
||||
}
|
||||
}
|
||||
}
|
||||
add_or_update_metadata {
|
||||
"2.13" {
|
||||
@@ -700,3 +766,51 @@ delete_metadata {
|
||||
}
|
||||
}
|
||||
}
|
||||
peek_task {
|
||||
"2.15" {
|
||||
description: "Peek the next task from a given queue"
|
||||
request {
|
||||
type: object
|
||||
required: [ queue ]
|
||||
properties {
|
||||
queue {
|
||||
description: "ID of the queue"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
task {
|
||||
description: "Task ID"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_num_entries {
|
||||
"2.15" {
|
||||
description: "Get the number of task entries in the given queue"
|
||||
request {
|
||||
type: object
|
||||
required: [ queue ]
|
||||
properties {
|
||||
queue {
|
||||
description: "ID of the queue"
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
type: object
|
||||
properties {
|
||||
num {
|
||||
description: "Number of entries"
|
||||
type: integer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
742
apiserver/schema/services/reports.conf
Normal file
742
apiserver/schema/services/reports.conf
Normal file
@@ -0,0 +1,742 @@
|
||||
_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
|
||||
additonalProperties: 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"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
page: [ page_size ]
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -685,7 +151,14 @@ get_all_ex {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all_ex."2.13"} {
|
||||
"2.14": ${get_all_ex."2.13"} {
|
||||
request.properties.search_hidden {
|
||||
description: "If set to 'true' then hidden tasks are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all_ex."2.14"} {
|
||||
request {
|
||||
properties {
|
||||
scroll_id {
|
||||
@@ -708,6 +181,15 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_all {
|
||||
"2.1" {
|
||||
@@ -822,7 +304,14 @@ get_all {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all."2.1"} {
|
||||
"2.14": ${get_all."2.1"} {
|
||||
request.properties.search_hidden {
|
||||
description: "If set to 'true' then hidden tasks are included in the search results"
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
}
|
||||
"2.15": ${get_all."2.14"} {
|
||||
request {
|
||||
properties {
|
||||
scroll_id {
|
||||
@@ -845,6 +334,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" {
|
||||
@@ -1213,6 +713,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
|
||||
@@ -1227,10 +731,6 @@ validate {
|
||||
"$ref": "#/definitions/configuration_item"
|
||||
}
|
||||
}
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
@@ -1378,6 +878,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
|
||||
@@ -1392,10 +896,6 @@ edit {
|
||||
"$ref": "#/definitions/configuration_item"
|
||||
}
|
||||
}
|
||||
script {
|
||||
description: "Script info"
|
||||
"$ref": "#/definitions/script"
|
||||
}
|
||||
}
|
||||
}
|
||||
response: ${_definitions.update_response}
|
||||
@@ -1464,6 +964,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 {
|
||||
@@ -1475,6 +979,13 @@ reset {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.21": ${reset."2.13"} {
|
||||
request.properties.delete_external_artifacts {
|
||||
description: "If set to 'true' then BE will try to delete the extenal artifacts associated with the task from the fileserver (if configured to do so)"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
reset_many {
|
||||
"2.13": ${_definitions.batch_operation} {
|
||||
@@ -1527,6 +1038,13 @@ reset_many {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.21": ${reset_many."2.13"} {
|
||||
request.properties.delete_external_artifacts {
|
||||
description: "If set to 'true' then BE will try to delete the extenal artifacts associated with the tasks from the fileserver (if configured to do so)"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
delete_many {
|
||||
"2.13": ${_definitions.batch_operation} {
|
||||
@@ -1577,6 +1095,13 @@ delete_many {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.21": ${delete_many."2.13"} {
|
||||
request.properties.delete_external_artifacts {
|
||||
description: "If set to 'true' then BE will try to delete the extenal artifacts associated with the tasks from the fileserver (if configured to do so)"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
delete {
|
||||
"2.1" {
|
||||
@@ -1630,6 +1155,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 {
|
||||
@@ -1641,6 +1170,13 @@ delete {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.21": ${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 task from the fileserver (if configured to do so)"
|
||||
type: boolean
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
archive {
|
||||
"2.12" {
|
||||
@@ -1694,12 +1230,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1884,7 +1420,7 @@ Fails if the following parameters in the task were not filled:
|
||||
]
|
||||
properties {
|
||||
queue {
|
||||
description: "Queue id. If not provided, task is added to the default queue."
|
||||
description: "Queue id. If not provided and no queue name is passed then task is added to the default queue."
|
||||
type: string
|
||||
}
|
||||
}
|
||||
@@ -1900,6 +1436,23 @@ Fails if the following parameters in the task were not filled:
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.19": ${enqueue."1.5"} {
|
||||
request.properties.queue_name {
|
||||
description: The name of the queue. If the queue does not exist then it is auto-created. Cannot be used together with the queue id
|
||||
type: string
|
||||
}
|
||||
}
|
||||
"2.22": ${enqueue."2.19"} {
|
||||
request.properties.verify_watched_queue {
|
||||
description: If passed then check wheter there are any workers watiching the queue
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
response.properties.queue_watched {
|
||||
description: Returns true if there are workers or autscalers working with the queue
|
||||
type: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
enqueue_many {
|
||||
"2.13": ${_definitions.change_many_request} {
|
||||
@@ -1908,7 +1461,7 @@ enqueue_many {
|
||||
properties {
|
||||
ids.description: "IDs of the tasks to enqueue"
|
||||
queue {
|
||||
description: "Queue id. If not provided, tasks are added to the default queue."
|
||||
description: "Queue id. If not provided and no queue name is passed then tasks are added to the default queue."
|
||||
type: string
|
||||
}
|
||||
validate_tasks {
|
||||
@@ -1927,6 +1480,23 @@ enqueue_many {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.19": ${enqueue_many."2.13"} {
|
||||
request.properties.queue_name {
|
||||
description: The name of the queue. If the queue does not exist then it is auto-created. Cannot be used together with the queue id
|
||||
type: string
|
||||
}
|
||||
}
|
||||
"2.22": ${enqueue_many."2.19"} {
|
||||
request.properties.verify_watched_queue {
|
||||
description: If passed then check wheter there are any workers watiching the queue
|
||||
type: boolean
|
||||
default: false
|
||||
}
|
||||
response.properties.queue_watched {
|
||||
description: Returns true if there are workers or autscalers working with the queue
|
||||
type: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
dequeue {
|
||||
"1.5" {
|
||||
@@ -1948,6 +1518,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} {
|
||||
@@ -1966,6 +1549,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" {
|
||||
@@ -2005,6 +1601,18 @@ completed {
|
||||
} ${_references.status_change_request}
|
||||
response: ${_definitions.update_response}
|
||||
}
|
||||
"2.20": ${completed."2.2"} {
|
||||
request.properties.publish {
|
||||
type: boolean
|
||||
default: false
|
||||
description: If set and the task is completed successfully then it is published
|
||||
}
|
||||
response.properties.published {
|
||||
description: "Number of tasks published (0 or 1)"
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ping {
|
||||
@@ -2427,7 +2035,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 {
|
||||
|
||||
@@ -139,6 +139,33 @@ get_current_user {
|
||||
}
|
||||
}
|
||||
}
|
||||
"2.20": ${get_current_user."2.1"} {
|
||||
response {
|
||||
properties {
|
||||
getting_started {
|
||||
type: object
|
||||
description: Getting stated info
|
||||
additionalProperties: true
|
||||
}
|
||||
created {
|
||||
type: string
|
||||
description: User creation time
|
||||
format: date-time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user