Add support for API v2.23 and attaching plots and scalars to models

This commit is contained in:
Alex Burlacu 2023-03-24 13:41:18 +02:00
parent b34063dd3f
commit 309b09f8a9
16 changed files with 36749 additions and 12 deletions

View File

@ -0,0 +1,764 @@
"""
auth service
This service provides authentication management and authorization
validation for the entire system.
"""
import six
from datetime import datetime
from dateutil.parser import parse as parse_datetime
from clearml.backend_api.session import (
Request,
Response,
NonStrictDataModel,
schema_property,
)
class Credentials(NonStrictDataModel):
"""
:param access_key: Credentials access key
:type access_key: str
:param secret_key: Credentials secret key
:type secret_key: str
:param label: Optional credentials label
:type label: str
"""
_schema = {
"properties": {
"access_key": {
"description": "Credentials access key",
"type": ["string", "null"],
},
"label": {
"description": "Optional credentials label",
"type": ["string", "null"],
},
"secret_key": {
"description": "Credentials secret key",
"type": ["string", "null"],
},
},
"type": "object",
}
def __init__(self, access_key=None, secret_key=None, label=None, **kwargs):
super(Credentials, self).__init__(**kwargs)
self.access_key = access_key
self.secret_key = secret_key
self.label = label
@schema_property("access_key")
def access_key(self):
return self._property_access_key
@access_key.setter
def access_key(self, value):
if value is None:
self._property_access_key = None
return
self.assert_isinstance(value, "access_key", six.string_types)
self._property_access_key = value
@schema_property("secret_key")
def secret_key(self):
return self._property_secret_key
@secret_key.setter
def secret_key(self, value):
if value is None:
self._property_secret_key = None
return
self.assert_isinstance(value, "secret_key", six.string_types)
self._property_secret_key = value
@schema_property("label")
def label(self):
return self._property_label
@label.setter
def label(self, value):
if value is None:
self._property_label = None
return
self.assert_isinstance(value, "label", six.string_types)
self._property_label = value
class CredentialKey(NonStrictDataModel):
"""
:param access_key:
:type access_key: str
:param label: Optional credentials label
:type label: str
:param last_used:
:type last_used: datetime.datetime
:param last_used_from:
:type last_used_from: str
"""
_schema = {
"properties": {
"access_key": {"description": "", "type": ["string", "null"]},
"label": {
"description": "Optional credentials label",
"type": ["string", "null"],
},
"last_used": {
"description": "",
"format": "date-time",
"type": ["string", "null"],
},
"last_used_from": {"description": "", "type": ["string", "null"]},
},
"type": "object",
}
def __init__(self, access_key=None, label=None, last_used=None, last_used_from=None, **kwargs):
super(CredentialKey, self).__init__(**kwargs)
self.access_key = access_key
self.label = label
self.last_used = last_used
self.last_used_from = last_used_from
@schema_property("access_key")
def access_key(self):
return self._property_access_key
@access_key.setter
def access_key(self, value):
if value is None:
self._property_access_key = None
return
self.assert_isinstance(value, "access_key", six.string_types)
self._property_access_key = value
@schema_property("label")
def label(self):
return self._property_label
@label.setter
def label(self, value):
if value is None:
self._property_label = None
return
self.assert_isinstance(value, "label", six.string_types)
self._property_label = value
@schema_property("last_used")
def last_used(self):
return self._property_last_used
@last_used.setter
def last_used(self, value):
if value is None:
self._property_last_used = None
return
self.assert_isinstance(value, "last_used", six.string_types + (datetime,))
if not isinstance(value, datetime):
value = parse_datetime(value)
self._property_last_used = value
@schema_property("last_used_from")
def last_used_from(self):
return self._property_last_used_from
@last_used_from.setter
def last_used_from(self, value):
if value is None:
self._property_last_used_from = None
return
self.assert_isinstance(value, "last_used_from", six.string_types)
self._property_last_used_from = value
class CreateCredentialsRequest(Request):
"""
Creates a new set of credentials for the authenticated user.
New key/secret is returned.
Note: Secret will never be returned in any other API call.
If a secret is lost or compromised, the key should be revoked
and a new set of credentials can be created.
:param label: Optional credentials label
:type label: str
"""
_service = "auth"
_action = "create_credentials"
_version = "2.23"
_schema = {
"additionalProperties": False,
"definitions": {},
"properties": {
"label": {
"description": "Optional credentials label",
"type": ["string", "null"],
}
},
"type": "object",
}
def __init__(self, label=None, **kwargs):
super(CreateCredentialsRequest, self).__init__(**kwargs)
self.label = label
@schema_property("label")
def label(self):
return self._property_label
@label.setter
def label(self, value):
if value is None:
self._property_label = None
return
self.assert_isinstance(value, "label", six.string_types)
self._property_label = value
class CreateCredentialsResponse(Response):
"""
Response of auth.create_credentials endpoint.
:param credentials: Created credentials
:type credentials: Credentials
"""
_service = "auth"
_action = "create_credentials"
_version = "2.23"
_schema = {
"definitions": {
"credentials": {
"properties": {
"access_key": {
"description": "Credentials access key",
"type": ["string", "null"],
},
"label": {
"description": "Optional credentials label",
"type": ["string", "null"],
},
"secret_key": {
"description": "Credentials secret key",
"type": ["string", "null"],
},
},
"type": "object",
}
},
"properties": {
"credentials": {
"description": "Created credentials",
"oneOf": [{"$ref": "#/definitions/credentials"}, {"type": "null"}],
}
},
"type": "object",
}
def __init__(self, credentials=None, **kwargs):
super(CreateCredentialsResponse, self).__init__(**kwargs)
self.credentials = credentials
@schema_property("credentials")
def credentials(self):
return self._property_credentials
@credentials.setter
def credentials(self, value):
if value is None:
self._property_credentials = None
return
if isinstance(value, dict):
value = Credentials.from_dict(value)
else:
self.assert_isinstance(value, "credentials", Credentials)
self._property_credentials = value
class EditCredentialsRequest(Request):
"""
Updates the label of the existing credentials for the authenticated user.
:param access_key: Existing credentials key
:type access_key: str
:param label: New credentials label
:type label: str
"""
_service = "auth"
_action = "edit_credentials"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"access_key": {"description": "Existing credentials key", "type": "string"},
"label": {"description": "New credentials label", "type": "string"},
},
"required": ["access_key"],
"type": "object",
}
def __init__(self, access_key, label=None, **kwargs):
super(EditCredentialsRequest, self).__init__(**kwargs)
self.access_key = access_key
self.label = label
@schema_property("access_key")
def access_key(self):
return self._property_access_key
@access_key.setter
def access_key(self, value):
if value is None:
self._property_access_key = None
return
self.assert_isinstance(value, "access_key", six.string_types)
self._property_access_key = value
@schema_property("label")
def label(self):
return self._property_label
@label.setter
def label(self, value):
if value is None:
self._property_label = None
return
self.assert_isinstance(value, "label", six.string_types)
self._property_label = value
class EditCredentialsResponse(Response):
"""
Response of auth.edit_credentials endpoint.
:param updated: Number of credentials updated
:type updated: int
"""
_service = "auth"
_action = "edit_credentials"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"updated": {
"description": "Number of credentials updated",
"enum": [0, 1],
"type": ["integer", "null"],
}
},
"type": "object",
}
def __init__(self, updated=None, **kwargs):
super(EditCredentialsResponse, self).__init__(**kwargs)
self.updated = updated
@schema_property("updated")
def updated(self):
return self._property_updated
@updated.setter
def updated(self, value):
if value is None:
self._property_updated = None
return
if isinstance(value, float) and value.is_integer():
value = int(value)
self.assert_isinstance(value, "updated", six.integer_types)
self._property_updated = value
class EditUserRequest(Request):
"""
Edit a users' auth data properties
:param user: User ID
:type user: str
:param role: The new user's role within the company
:type role: str
"""
_service = "auth"
_action = "edit_user"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"role": {
"description": "The new user's role within the company",
"enum": ["admin", "superuser", "user", "annotator"],
"type": ["string", "null"],
},
"user": {"description": "User ID", "type": ["string", "null"]},
},
"type": "object",
}
def __init__(self, user=None, role=None, **kwargs):
super(EditUserRequest, self).__init__(**kwargs)
self.user = user
self.role = role
@schema_property("user")
def user(self):
return self._property_user
@user.setter
def user(self, value):
if value is None:
self._property_user = None
return
self.assert_isinstance(value, "user", six.string_types)
self._property_user = value
@schema_property("role")
def role(self):
return self._property_role
@role.setter
def role(self, value):
if value is None:
self._property_role = None
return
self.assert_isinstance(value, "role", six.string_types)
self._property_role = value
class EditUserResponse(Response):
"""
Response of auth.edit_user endpoint.
:param updated: Number of users updated (0 or 1)
:type updated: float
:param fields: Updated fields names and values
:type fields: dict
"""
_service = "auth"
_action = "edit_user"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"fields": {
"additionalProperties": True,
"description": "Updated fields names and values",
"type": ["object", "null"],
},
"updated": {
"description": "Number of users updated (0 or 1)",
"enum": [0, 1],
"type": ["number", "null"],
},
},
"type": "object",
}
def __init__(self, updated=None, fields=None, **kwargs):
super(EditUserResponse, self).__init__(**kwargs)
self.updated = updated
self.fields = fields
@schema_property("updated")
def updated(self):
return self._property_updated
@updated.setter
def updated(self, value):
if value is None:
self._property_updated = None
return
self.assert_isinstance(value, "updated", six.integer_types + (float,))
self._property_updated = value
@schema_property("fields")
def fields(self):
return self._property_fields
@fields.setter
def fields(self, value):
if value is None:
self._property_fields = None
return
self.assert_isinstance(value, "fields", (dict,))
self._property_fields = value
class GetCredentialsRequest(Request):
"""
Returns all existing credential keys for the authenticated user.
Note: Only credential keys are returned.
"""
_service = "auth"
_action = "get_credentials"
_version = "2.23"
_schema = {
"additionalProperties": False,
"definitions": {},
"properties": {},
"type": "object",
}
class GetCredentialsResponse(Response):
"""
Response of auth.get_credentials endpoint.
:param credentials: List of credentials, each with an empty secret field.
:type credentials: Sequence[CredentialKey]
"""
_service = "auth"
_action = "get_credentials"
_version = "2.23"
_schema = {
"definitions": {
"credential_key": {
"properties": {
"access_key": {"description": "", "type": ["string", "null"]},
"label": {
"description": "Optional credentials label",
"type": ["string", "null"],
},
"last_used": {
"description": "",
"format": "date-time",
"type": ["string", "null"],
},
"last_used_from": {"description": "", "type": ["string", "null"]},
},
"type": "object",
}
},
"properties": {
"credentials": {
"description": "List of credentials, each with an empty secret field.",
"items": {"$ref": "#/definitions/credential_key"},
"type": ["array", "null"],
}
},
"type": "object",
}
def __init__(self, credentials=None, **kwargs):
super(GetCredentialsResponse, self).__init__(**kwargs)
self.credentials = credentials
@schema_property("credentials")
def credentials(self):
return self._property_credentials
@credentials.setter
def credentials(self, value):
if value is None:
self._property_credentials = None
return
self.assert_isinstance(value, "credentials", (list, tuple))
if any(isinstance(v, dict) for v in value):
value = [CredentialKey.from_dict(v) if isinstance(v, dict) else v for v in value]
else:
self.assert_isinstance(value, "credentials", CredentialKey, is_array=True)
self._property_credentials = value
class LoginRequest(Request):
"""
Get a token based on supplied credentials (key/secret).
Intended for use by users with key/secret credentials that wish to obtain a token
for use with other services.
:param expiration_sec: Requested token expiration time in seconds. Not
guaranteed, might be overridden by the service
:type expiration_sec: int
"""
_service = "auth"
_action = "login"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"expiration_sec": {
"description": (
"Requested token expiration time in seconds. \n Not guaranteed, might be "
"overridden by the service"
),
"type": ["integer", "null"],
}
},
"type": "object",
}
def __init__(self, expiration_sec=None, **kwargs):
super(LoginRequest, self).__init__(**kwargs)
self.expiration_sec = expiration_sec
@schema_property("expiration_sec")
def expiration_sec(self):
return self._property_expiration_sec
@expiration_sec.setter
def expiration_sec(self, value):
if value is None:
self._property_expiration_sec = None
return
if isinstance(value, float) and value.is_integer():
value = int(value)
self.assert_isinstance(value, "expiration_sec", six.integer_types)
self._property_expiration_sec = value
class LoginResponse(Response):
"""
Response of auth.login endpoint.
:param token: Token string
:type token: str
"""
_service = "auth"
_action = "login"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {"token": {"description": "Token string", "type": ["string", "null"]}},
"type": "object",
}
def __init__(self, token=None, **kwargs):
super(LoginResponse, self).__init__(**kwargs)
self.token = token
@schema_property("token")
def token(self):
return self._property_token
@token.setter
def token(self, value):
if value is None:
self._property_token = None
return
self.assert_isinstance(value, "token", six.string_types)
self._property_token = value
class RevokeCredentialsRequest(Request):
"""
Revokes (and deletes) a set (key, secret) of credentials for
the authenticated user.
:param access_key: Credentials key
:type access_key: str
"""
_service = "auth"
_action = "revoke_credentials"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {"access_key": {"description": "Credentials key", "type": ["string", "null"]}},
"required": ["key_id"],
"type": "object",
}
def __init__(self, access_key=None, **kwargs):
super(RevokeCredentialsRequest, self).__init__(**kwargs)
self.access_key = access_key
@schema_property("access_key")
def access_key(self):
return self._property_access_key
@access_key.setter
def access_key(self, value):
if value is None:
self._property_access_key = None
return
self.assert_isinstance(value, "access_key", six.string_types)
self._property_access_key = value
class RevokeCredentialsResponse(Response):
"""
Response of auth.revoke_credentials endpoint.
:param revoked: Number of credentials revoked
:type revoked: int
"""
_service = "auth"
_action = "revoke_credentials"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"revoked": {
"description": "Number of credentials revoked",
"enum": [0, 1],
"type": ["integer", "null"],
}
},
"type": "object",
}
def __init__(self, revoked=None, **kwargs):
super(RevokeCredentialsResponse, self).__init__(**kwargs)
self.revoked = revoked
@schema_property("revoked")
def revoked(self):
return self._property_revoked
@revoked.setter
def revoked(self, value):
if value is None:
self._property_revoked = None
return
if isinstance(value, float) and value.is_integer():
value = int(value)
self.assert_isinstance(value, "revoked", six.integer_types)
self._property_revoked = value
response_mapping = {
LoginRequest: LoginResponse,
CreateCredentialsRequest: CreateCredentialsResponse,
GetCredentialsRequest: GetCredentialsResponse,
EditCredentialsRequest: EditCredentialsResponse,
RevokeCredentialsRequest: RevokeCredentialsResponse,
EditUserRequest: EditUserResponse,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
"""
organization service
This service provides organization level operations
"""
import six
from clearml.backend_api.session import Request, Response, schema_property
class GetTagsRequest(Request):
"""
Get all the user and system tags used for the company tasks and models
:param include_system: If set to 'true' then the list of the system tags is
also returned. The default value is 'false'
:type include_system: bool
:param filter: Filter on entities to collect tags from
:type filter: dict
"""
_service = "organization"
_action = "get_tags"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"filter": {
"description": "Filter on entities to collect tags from",
"properties": {
"system_tags": {
"description": (
"The list of system tag values to filter by. Use 'null' value to specify empty system tags."
" Use '__Snot' value to specify that the following value should be excluded"
),
"items": {"type": "string"},
"type": "array",
},
"tags": {
"description": (
"The list of tag values to filter by. Use 'null' value to specify empty tags. Use '__Snot'"
" value to specify that the following value should be excluded"
),
"items": {"type": "string"},
"type": "array",
},
},
"type": ["object", "null"],
},
"include_system": {
"default": False,
"description": (
"If set to 'true' then the list of the system tags is also returned. The default value is 'false'"
),
"type": ["boolean", "null"],
},
},
"type": "object",
}
def __init__(self, include_system=False, filter=None, **kwargs):
super(GetTagsRequest, self).__init__(**kwargs)
self.include_system = include_system
self.filter = filter
@schema_property("include_system")
def include_system(self):
return self._property_include_system
@include_system.setter
def include_system(self, value):
if value is None:
self._property_include_system = None
return
self.assert_isinstance(value, "include_system", (bool,))
self._property_include_system = value
@schema_property("filter")
def filter(self):
return self._property_filter
@filter.setter
def filter(self, value):
if value is None:
self._property_filter = None
return
self.assert_isinstance(value, "filter", (dict,))
self._property_filter = value
class GetTagsResponse(Response):
"""
Response of organization.get_tags endpoint.
:param tags: The list of unique tag values
:type tags: Sequence[str]
:param system_tags: The list of unique system tag values. Returned only if
'include_system' is set to 'true' in the request
:type system_tags: Sequence[str]
"""
_service = "organization"
_action = "get_tags"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"system_tags": {
"description": (
"The list of unique system tag values. Returned only if 'include_system' is set to 'true' in the"
" request"
),
"items": {"type": "string"},
"type": ["array", "null"],
},
"tags": {
"description": "The list of unique tag values",
"items": {"type": "string"},
"type": ["array", "null"],
},
},
"type": "object",
}
def __init__(self, tags=None, system_tags=None, **kwargs):
super(GetTagsResponse, self).__init__(**kwargs)
self.tags = tags
self.system_tags = system_tags
@schema_property("tags")
def tags(self):
return self._property_tags
@tags.setter
def tags(self, value):
if value is None:
self._property_tags = None
return
self.assert_isinstance(value, "tags", (list, tuple))
self.assert_isinstance(value, "tags", six.string_types, is_array=True)
self._property_tags = value
@schema_property("system_tags")
def system_tags(self):
return self._property_system_tags
@system_tags.setter
def system_tags(self, value):
if value is None:
self._property_system_tags = None
return
self.assert_isinstance(value, "system_tags", (list, tuple))
self.assert_isinstance(value, "system_tags", six.string_types, is_array=True)
self._property_system_tags = value
response_mapping = {
GetTagsRequest: GetTagsResponse,
}

View File

@ -0,0 +1,169 @@
"""
pipelines service
Provides a management API for pipelines in the system.
"""
import six
from clearml.backend_api.session import (
Request,
Response,
schema_property,
)
class StartPipelineRequest(Request):
"""
Start a pipeline
:param task: ID of the task on which the pipeline will be based
:type task: str
:param queue: Queue ID in which the created pipeline task will be enqueued
:type queue: str
:param args: Task arguments, name/value to be placed in the hyperparameters
Args section
:type args: Sequence[dict]
"""
_service = "pipelines"
_action = "start_pipeline"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"args": {
"description": "Task arguments, name/value to be placed in the hyperparameters Args section",
"items": {
"properties": {
"name": {"type": "string"},
"value": {"type": ["string", "null"]},
},
"type": "object",
},
"type": "array",
},
"queue": {
"description": "Queue ID in which the created pipeline task will be enqueued",
"type": "string",
},
"task": {
"description": "ID of the task on which the pipeline will be based",
"type": "string",
},
},
"required": ["task"],
"type": "object",
}
def __init__(self, task, queue=None, args=None, **kwargs):
super(StartPipelineRequest, self).__init__(**kwargs)
self.task = task
self.queue = queue
self.args = args
@schema_property("task")
def task(self):
return self._property_task
@task.setter
def task(self, value):
if value is None:
self._property_task = None
return
self.assert_isinstance(value, "task", six.string_types)
self._property_task = value
@schema_property("queue")
def queue(self):
return self._property_queue
@queue.setter
def queue(self, value):
if value is None:
self._property_queue = None
return
self.assert_isinstance(value, "queue", six.string_types)
self._property_queue = value
@schema_property("args")
def args(self):
return self._property_args
@args.setter
def args(self, value):
if value is None:
self._property_args = None
return
self.assert_isinstance(value, "args", (list, tuple))
self.assert_isinstance(value, "args", (dict,), is_array=True)
self._property_args = value
class StartPipelineResponse(Response):
"""
Response of pipelines.start_pipeline endpoint.
:param pipeline: ID of the new pipeline task
:type pipeline: str
:param enqueued: True if the task was successfully enqueued
:type enqueued: bool
"""
_service = "pipelines"
_action = "start_pipeline"
_version = "2.23"
_schema = {
"definitions": {},
"properties": {
"enqueued": {
"description": "True if the task was successfully enqueued",
"type": ["boolean", "null"],
},
"pipeline": {
"description": "ID of the new pipeline task",
"type": ["string", "null"],
},
},
"type": "object",
}
def __init__(self, pipeline=None, enqueued=None, **kwargs):
super(StartPipelineResponse, self).__init__(**kwargs)
self.pipeline = pipeline
self.enqueued = enqueued
@schema_property("pipeline")
def pipeline(self):
return self._property_pipeline
@pipeline.setter
def pipeline(self, value):
if value is None:
self._property_pipeline = None
return
self.assert_isinstance(value, "pipeline", six.string_types)
self._property_pipeline = value
@schema_property("enqueued")
def enqueued(self):
return self._property_enqueued
@enqueued.setter
def enqueued(self, value):
if value is None:
self._property_enqueued = None
return
self.assert_isinstance(value, "enqueued", (bool,))
self._property_enqueued = value
response_mapping = {
StartPipelineRequest: StartPipelineResponse,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -89,7 +89,13 @@ class CompoundRequest(Request):
return item
def to_dict(self):
return self._get_item().to_dict()
dict_ = self._get_item().to_dict()
dict_properties = super(Request, self).to_dict()
if self._item_prop_name in dict_properties:
del dict_properties[self._item_prop_name]
dict_.update(dict_properties)
return dict_
def validate(self):
return self._get_item().validate(self._schema)

View File

@ -77,7 +77,7 @@ class MetricsEventAdapter(object):
def variant(self):
return self._variant
def __init__(self, metric, variant, iter=None, timestamp=None, task=None, gen_timestamp_if_none=True):
def __init__(self, metric, variant, iter=None, timestamp=None, task=None, gen_timestamp_if_none=True, , model_event=None):
if not timestamp and gen_timestamp_if_none:
timestamp = int(time.time() * 1000)
self._metric = metric
@ -85,6 +85,7 @@ class MetricsEventAdapter(object):
self._iter = iter
self._timestamp = timestamp
self._task = task
self._model_event = model_event
# Try creating an event just to trigger validation
_ = self.get_api_event()
@ -119,6 +120,8 @@ class MetricsEventAdapter(object):
)
if self._iter is not None:
res.update(iter=self._iter)
if self._model_event is not None:
res.update(model_event=self._model_event)
return res
@classmethod

View File

@ -47,7 +47,7 @@ class Metrics(InterfaceBase):
finally:
self._storage_lock.release()
def __init__(self, session, task, storage_uri, storage_uri_suffix='metrics', iteration_offset=0, log=None):
def __init__(self, session, task, storage_uri, storage_uri_suffix='metrics', iteration_offset=0, log=None, for_model=False):
super(Metrics, self).__init__(session, log=log)
self._task_id = task.id
self._task_iteration_offset = iteration_offset
@ -56,6 +56,7 @@ class Metrics(InterfaceBase):
self._file_related_event_time = None
self._file_upload_time = None
self._offline_log_filename = None
self._for_model = for_model
if self._offline_mode:
offline_folder = Path(task.get_offline_mode_folder())
offline_folder.mkdir(parents=True, exist_ok=True)
@ -213,7 +214,10 @@ class Metrics(InterfaceBase):
if good_events:
_events = [ev.get_api_event() for ev in good_events]
batched_requests = [api_events.AddRequest(event=ev) for ev in _events if ev]
additional_kwargs = {}
if Session.check_min_api_version("2.23"):
additional_kwargs["model_event"] = self._for_model
batched_requests = [api_events.AddRequest(event=ev, **additional_kwargs) for ev in _events if ev]
if batched_requests:
if self._offline_mode:
with open(self._offline_log_filename.as_posix(), 'at') as f:

View File

@ -33,9 +33,8 @@ except ImportError:
class BackgroundReportService(BackgroundMonitor, AsyncManagerMixin):
__daemon_live_check_timeout = 10.0
def __init__(self, task, async_enable, metrics, flush_frequency, flush_threshold):
super(BackgroundReportService, self).__init__(
task=task, wait_period=flush_frequency)
def __init__(self, task, async_enable, metrics, flush_frequency, flush_threshold, for_model=False):
super(BackgroundReportService, self).__init__(task=task, wait_period=flush_frequency, for_model=for_model)
self._flush_threshold = flush_threshold
self._flush_event = ForkEvent()
self._empty_state_event = ForkEvent()
@ -250,7 +249,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan
reporter.flush()
"""
def __init__(self, metrics, task, async_enable=False):
def __init__(self, metrics, task, async_enable=False, for_model=False):
"""
Create a reporter
:param metrics: A Metrics manager instance that handles actual reporting, uploads etc.
@ -269,10 +268,12 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan
self._async_enable = async_enable
self._flush_frequency = 5.0
self._max_iteration = 0
self._for_model = for_model
flush_threshold = config.get("development.worker.report_event_flush_threshold", 100)
self._report_service = BackgroundReportService(
task=task, async_enable=async_enable, metrics=metrics,
flush_frequency=self._flush_frequency, flush_threshold=flush_threshold)
flush_frequency=self._flush_frequency,
flush_threshold=flush_threshold, for_model=for_model)
self._report_service.start()
def _set_storage_uri(self, value):

View File

@ -6,7 +6,14 @@ import shutil
from tempfile import mkdtemp, mkstemp
import six
from typing import List, Dict, Union, Optional, Mapping, TYPE_CHECKING, Sequence, Any
import math
from typing import List, Dict, Union, Optional, Mapping, TYPE_CHECKING, Sequence, Any, Tuple
import numpy as np
try:
import pandas as pd
except ImportError:
pd = None
from .backend_api import Session
from .backend_api.services import models, projects
@ -14,11 +21,13 @@ from pathlib2 import Path
from .utilities.config import config_dict_to_text, text_to_config_dict
from .utilities.proxy_object import cast_basic_type
from .utilities.plotly_reporter import SeriesInfo
from .backend_interface.util import (
validate_dict, get_single_result, mutually_exclusive, exact_match_regex,
get_or_create_project, )
from .debugging.log import get_logger
from .errors import UsageError
from .storage.cache import CacheManager
from .storage.helper import StorageHelper
from .storage.util import get_common_path
@ -26,6 +35,7 @@ from .utilities.enum import Options
from .backend_interface import Task as _Task
from .backend_interface.model import create_dummy_model, Model as _Model
from .backend_interface.session import SendError
from .backend_interface.metrics import Reporter, Metrics
from .config import running_remotely, get_cache_dir
@ -329,6 +339,7 @@ class BaseModel(object):
self._log = get_logger()
self._task = None
self._reload_required = False
self._reporter = None
self._set_task(task)
def get_weights(self, raise_on_error=False, force_download=False):
@ -400,6 +411,616 @@ class BaseModel(object):
target_files = list(Path(target_folder).glob('*'))
return target_files
def report_scalar(self, title, series, value, iteration):
# type: (str, str, float, int) -> None
"""
For explicit reporting, plot a scalar series.
:param str title: The title (metric) of the plot. Plot more than one scalar series on the same plot by using
the same ``title`` for each call to this method.
:param str series: The series name (variant) of the reported scalar.
:param float value: The value to plot per iteration.
:param int iteration: The reported iteration / step (x-axis of the reported time series)
"""
self._init_reporter()
return self._reporter.report_scalar(title=title, series=series, value=float(value), iter=iteration)
def report_single_value(self, name, value):
# type: (str, float) -> None
"""
Reports a single value metric (for example, total experiment accuracy or mAP)
:param name: Metric's name
:param value: Metric's value
"""
self._init_reporter()
return self._reporter.report_scalar(title="Summary", series=name, value=float(value), iter=-2**31)
def report_histogram(
self,
title, # type: str
series, # type: str
values, # type: Sequence[Union[int, float]]
iteration=None, # type: Optional[int]
labels=None, # type: Optional[List[str]]
xlabels=None, # type: Optional[List[str]]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
mode=None, # type: Optional[str]
data_args=None, # type: Optional[dict]
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, plot a (default grouped) histogram.
Notice this function will not calculate the histogram,
it assumes the histogram was already calculated in `values`
For example:
.. code-block:: py
vector_series = np.random.randint(10, size=10).reshape(2,5)
model.report_histogram(title='histogram example', series='histogram series',
values=vector_series, iteration=0, labels=['A','B'], xaxis='X axis label', yaxis='Y axis label')
:param title: The title (metric) of the plot.
:param series: The series name (variant) of the reported histogram.
:param values: The series values. A list of floats, or an N-dimensional Numpy array containing
data for each histogram bar.
:param iteration: The reported iteration / step. Each ``iteration`` creates another plot.
:param labels: Labels for each bar group, creating a plot legend labeling each series. (Optional)
:param xlabels: Labels per entry in each bucket in the histogram (vector), creating a set of labels
for each histogram bar on the x-axis. (Optional)
:param xaxis: The x-axis title. (Optional)
:param yaxis: The y-axis title. (Optional)
:param mode: Multiple histograms mode, stack / group / relative. Default is 'group'.
:param data_args: optional dictionary for data configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/bar/
example: data_args={'orientation': 'h', 'marker': {'color': 'blue'}}
:param extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/bar/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
if not isinstance(values, np.ndarray):
values = np.array(values)
return self._reporter.report_histogram(
title=title,
series=series,
histogram=values,
iter=iteration or 0,
labels=labels,
xlabels=xlabels,
xtitle=xaxis,
ytitle=yaxis,
mode=mode or "group",
data_args=data_args,
layout_config=extra_layout
)
def report_vector(
self,
title, # type: str
series, # type: str
values, # type: Sequence[Union[int, float]]
iteration=None, # type: Optional[int]
labels=None, # type: Optional[List[str]]
xlabels=None, # type: Optional[List[str]]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
mode=None, # type: Optional[str]
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, plot a vector as (default stacked) histogram.
For example:
.. code-block:: py
vector_series = np.random.randint(10, size=10).reshape(2,5)
model.report_vector(title='vector example', series='vector series', values=vector_series, iteration=0,
labels=['A','B'], xaxis='X axis label', yaxis='Y axis label')
:param title: The title (metric) of the plot.
:param series: The series name (variant) of the reported histogram.
:param values: The series values. A list of floats, or an N-dimensional Numpy array containing
data for each histogram bar.
:param iteration: The reported iteration / step. Each ``iteration`` creates another plot.
:param labels: Labels for each bar group, creating a plot legend labeling each series. (Optional)
:param xlabels: Labels per entry in each bucket in the histogram (vector), creating a set of labels
for each histogram bar on the x-axis. (Optional)
:param xaxis: The x-axis title. (Optional)
:param yaxis: The y-axis title. (Optional)
:param mode: Multiple histograms mode, stack / group / relative. Default is 'group'.
:param extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/layout/
example: extra_layout={'showlegend': False, 'plot_bgcolor': 'yellow'}
"""
self._init_reporter()
return self.report_histogram(
title,
series,
values,
iteration or 0,
labels=labels,
xlabels=xlabels,
xaxis=xaxis,
yaxis=yaxis,
mode=mode,
extra_layout=extra_layout,
)
def report_table(
self,
title, # type: str
series, # type: str
iteration=None, # type: Optional[int]
table_plot=None, # type: Optional[pd.DataFrame, Sequence[Sequence]]
csv=None, # type: Optional[str]
url=None, # type: Optional[str]
extra_layout=None # type: Optional[dict]
):
"""
For explicit report, report a table plot.
One and only one of the following parameters must be provided.
- ``table_plot`` - Pandas DataFrame or Table as list of rows (list)
- ``csv`` - CSV file
- ``url`` - URL to CSV file
For example:
.. code-block:: py
df = pd.DataFrame({'num_legs': [2, 4, 8, 0],
'num_wings': [2, 0, 0, 0],
'num_specimen_seen': [10, 2, 1, 8]},
index=['falcon', 'dog', 'spider', 'fish'])
model.report_table(title='table example',series='pandas DataFrame',iteration=0,table_plot=df)
:param title: The title (metric) of the table.
:param series: The series name (variant) of the reported table.
:param iteration: The reported iteration / step.
:param table_plot: The output table plot object
:param csv: path to local csv file
:param url: A URL to the location of csv file.
:param extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/layout/
example: extra_layout={'height': 600}
"""
mutually_exclusive(
UsageError, _check_none=True,
table_plot=table_plot, csv=csv, url=url
)
table = table_plot
if url or csv:
if not pd:
raise UsageError(
"pandas is required in order to support reporting tables using CSV or a URL, "
"please install the pandas python package"
)
if url:
table = pd.read_csv(url, index_col=[0])
elif csv:
table = pd.read_csv(csv, index_col=[0])
def replace(dst, *srcs):
for src in srcs:
reporter_table.replace(src, dst, inplace=True)
if isinstance(table, (list, tuple)):
reporter_table = table
else:
reporter_table = table.fillna(str(np.nan))
replace("NaN", np.nan, math.nan if six.PY3 else float("nan"))
replace("Inf", np.inf, math.inf if six.PY3 else float("inf"))
replace("-Inf", -np.inf, np.NINF, -math.inf if six.PY3 else -float("inf"))
self._init_reporter()
return self._reporter.report_table(
title=title,
series=series,
table=reporter_table,
iteration=iteration or 0,
layout_config=extra_layout
)
def report_line_plot(
self,
title, # type: str
series, # type: Sequence[SeriesInfo]
xaxis, # type: str
yaxis, # type: str
mode="lines", # type: str
iteration=None, # type: Optional[int]
reverse_xaxis=False, # type: bool
comment=None, # type: Optional[str]
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, plot one or more series as lines.
:param str title: The title (metric) of the plot.
:param list series: All the series data, one list element for each line in the plot.
:param int iteration: The reported iteration / step.
:param str xaxis: The x-axis title. (Optional)
:param str yaxis: The y-axis title. (Optional)
:param str mode: The type of line plot.
The values are:
- ``lines`` (default)
- ``markers``
- ``lines+markers``
:param bool reverse_xaxis: Reverse the x-axis
The values are:
- ``True`` - The x-axis is high to low (reversed).
- ``False`` - The x-axis is low to high (not reversed). (default)
:param str comment: A comment displayed with the plot, underneath the title.
:param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/scatter/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
# noinspection PyArgumentList
series = [SeriesInfo(**s) if isinstance(s, dict) else s for s in series]
return self._reporter.report_line_plot(
title=title,
series=series,
iter=iteration or 0,
xtitle=xaxis,
ytitle=yaxis,
mode=mode,
reverse_xaxis=reverse_xaxis,
comment=comment,
layout_config=extra_layout
)
def report_scatter2d(
self,
title, # type: str
series, # type: str
scatter, # type: Union[Sequence[Tuple[float, float]], np.ndarray]
iteration=None, # type: Optional[int]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
labels=None, # type: Optional[List[str]]
mode="line", # type: str
comment=None, # type: Optional[str]
extra_layout=None, # type: Optional[dict]
):
"""
For explicit reporting, report a 2d scatter plot.
For example:
.. code-block:: py
scatter2d = np.hstack((np.atleast_2d(np.arange(0, 10)).T, np.random.randint(10, size=(10, 1))))
model.report_scatter2d(title="example_scatter", series="series", iteration=0, scatter=scatter2d,
xaxis="title x", yaxis="title y")
Plot multiple 2D scatter series on the same plot by passing the same ``title`` and ``iteration`` values
to this method:
.. code-block:: py
scatter2d_1 = np.hstack((np.atleast_2d(np.arange(0, 10)).T, np.random.randint(10, size=(10, 1))))
model.report_scatter2d(title="example_scatter", series="series_1", iteration=1, scatter=scatter2d_1,
xaxis="title x", yaxis="title y")
scatter2d_2 = np.hstack((np.atleast_2d(np.arange(0, 10)).T, np.random.randint(10, size=(10, 1))))
model.report_scatter2d("example_scatter", "series_2", iteration=1, scatter=scatter2d_2,
xaxis="title x", yaxis="title y")
:param str title: The title (metric) of the plot.
:param str series: The series name (variant) of the reported scatter plot.
:param list scatter: The scatter data. numpy.ndarray or list of (pairs of x,y) scatter:
:param int iteration: The reported iteration / step.
:param str xaxis: The x-axis title. (Optional)
:param str yaxis: The y-axis title. (Optional)
:param list(str) labels: Labels per point in the data assigned to the ``scatter`` parameter. The labels must be
in the same order as the data.
:param str mode: The type of scatter plot. The values are:
- ``lines``
- ``markers``
- ``lines+markers``
:param str comment: A comment displayed with the plot, underneath the title.
:param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/scatter/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
if not isinstance(scatter, np.ndarray):
if not isinstance(scatter, list):
scatter = list(scatter)
scatter = np.array(scatter)
return self._reporter.report_2d_scatter(
title=title,
series=series,
data=scatter,
iter=iteration or 0,
mode=mode,
xtitle=xaxis,
ytitle=yaxis,
labels=labels,
comment=comment,
layout_config=extra_layout,
)
def report_scatter3d(
self,
title, # type: str
series, # type: str
scatter, # type: Union[Sequence[Tuple[float, float, float]], np.ndarray]
iteration=None, # type: Optional[int]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
zaxis=None, # type: Optional[str]
labels=None, # type: Optional[List[str]]
mode="markers", # type: str
fill=False, # type: bool
comment=None, # type: Optional[str]
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, plot a 3d scatter graph (with markers).
:param str title: The title (metric) of the plot.
:param str series: The series name (variant) of the reported scatter plot.
:param Union[numpy.ndarray, list] scatter: The scatter data.
list of (pairs of x,y,z), list of series [[(x1,y1,z1)...]], or numpy.ndarray
:param int iteration: The reported iteration / step.
:param str xaxis: The x-axis title. (Optional)
:param str yaxis: The y-axis title. (Optional)
:param str zaxis: The z-axis title. (Optional)
:param list(str) labels: Labels per point in the data assigned to the ``scatter`` parameter. The labels must be
in the same order as the data.
:param str mode: The type of scatter plot. The values are:
- ``lines``
- ``markers``
- ``lines+markers``
For example:
.. code-block:: py
scatter3d = np.random.randint(10, size=(10, 3))
model.report_scatter3d(title="example_scatter_3d", series="series_xyz", iteration=1, scatter=scatter3d,
xaxis="title x", yaxis="title y", zaxis="title z")
:param bool fill: Fill the area under the curve. The values are:
- ``True`` - Fill
- ``False`` - Do not fill (default)
:param str comment: A comment displayed with the plot, underneath the title.
:param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/scatter3d/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
# check if multiple series
multi_series = (
isinstance(scatter, list)
and (
isinstance(scatter[0], np.ndarray)
or (
scatter[0]
and isinstance(scatter[0], list)
and isinstance(scatter[0][0], list)
)
)
)
if not multi_series:
if not isinstance(scatter, np.ndarray):
if not isinstance(scatter, list):
scatter = list(scatter)
scatter = np.array(scatter)
try:
scatter = scatter.astype(np.float32)
except ValueError:
pass
return self._reporter.report_3d_scatter(
title=title,
series=series,
data=scatter,
iter=iteration or 0,
labels=labels,
mode=mode,
fill=fill,
comment=comment,
xtitle=xaxis,
ytitle=yaxis,
ztitle=zaxis,
layout_config=extra_layout
)
def report_confusion_matrix(
self,
title, # type: str
series, # type: str
matrix, # type: np.ndarray
iteration=None, # type: Optional[int]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
xlabels=None, # type: Optional[List[str]]
ylabels=None, # type: Optional[List[str]]
yaxis_reversed=False, # type: bool
comment=None, # type: Optional[str]
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, plot a heat-map matrix.
For example:
.. code-block:: py
confusion = np.random.randint(10, size=(10, 10))
model.report_confusion_matrix("example confusion matrix", "ignored", iteration=1, matrix=confusion,
xaxis="title X", yaxis="title Y")
:param str title: The title (metric) of the plot.
:param str series: The series name (variant) of the reported confusion matrix.
:param numpy.ndarray matrix: A heat-map matrix (example: confusion matrix)
:param int iteration: The reported iteration / step.
:param str xaxis: The x-axis title. (Optional)
:param str yaxis: The y-axis title. (Optional)
:param list(str) xlabels: Labels for each column of the matrix. (Optional)
:param list(str) ylabels: Labels for each row of the matrix. (Optional)
:param bool yaxis_reversed: If False 0,0 is at the bottom left corner. If True, 0,0 is at the top left corner
:param str comment: A comment displayed with the plot, underneath the title.
:param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/heatmap/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
if not isinstance(matrix, np.ndarray):
matrix = np.array(matrix)
return self._reporter.report_value_matrix(
title=title,
series=series,
data=matrix.astype(np.float32),
iter=iteration or 0,
xtitle=xaxis,
ytitle=yaxis,
xlabels=xlabels,
ylabels=ylabels,
yaxis_reversed=yaxis_reversed,
comment=comment,
layout_config=extra_layout
)
def report_matrix(
self,
title, # type: str
series, # type: str
matrix, # type: np.ndarray
iteration=None, # type: Optional[int]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
xlabels=None, # type: Optional[List[str]]
ylabels=None, # type: Optional[List[str]]
yaxis_reversed=False, # type: bool
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, plot a confusion matrix.
.. note::
This method is the same as :meth:`Model.report_confusion_matrix`.
:param str title: The title (metric) of the plot.
:param str series: The series name (variant) of the reported confusion matrix.
:param numpy.ndarray matrix: A heat-map matrix (example: confusion matrix)
:param int iteration: The reported iteration / step.
:param str xaxis: The x-axis title. (Optional)
:param str yaxis: The y-axis title. (Optional)
:param list(str) xlabels: Labels for each column of the matrix. (Optional)
:param list(str) ylabels: Labels for each row of the matrix. (Optional)
:param bool yaxis_reversed: If False, 0,0 is at the bottom left corner. If True, 0,0 is at the top left corner
:param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/heatmap/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
return self.report_confusion_matrix(
title,
series,
matrix,
iteration or 0,
xaxis=xaxis,
yaxis=yaxis,
xlabels=xlabels,
ylabels=ylabels,
yaxis_reversed=yaxis_reversed,
extra_layout=extra_layout
)
def report_surface(
self,
title, # type: str
series, # type: str
matrix, # type: np.ndarray
iteration=None, # type: Optional[int]
xaxis=None, # type: Optional[str]
yaxis=None, # type: Optional[str]
zaxis=None, # type: Optional[str]
xlabels=None, # type: Optional[List[str]]
ylabels=None, # type: Optional[List[str]]
camera=None, # type: Optional[Sequence[float]]
comment=None, # type: Optional[str]
extra_layout=None # type: Optional[dict]
):
"""
For explicit reporting, report a 3d surface plot.
.. note::
This method plots the same data as :meth:`Model.report_confusion_matrix`, but presents the
data as a surface diagram not a confusion matrix.
.. code-block:: py
surface_matrix = np.random.randint(10, size=(10, 10))
model.report_surface("example surface", "series", iteration=0, matrix=surface_matrix,
xaxis="title X", yaxis="title Y", zaxis="title Z")
:param str title: The title (metric) of the plot.
:param str series: The series name (variant) of the reported surface.
:param numpy.ndarray matrix: A heat-map matrix (example: confusion matrix)
:param int iteration: The reported iteration / step.
:param str xaxis: The x-axis title. (Optional)
:param str yaxis: The y-axis title. (Optional)
:param str zaxis: The z-axis title. (Optional)
:param list(str) xlabels: Labels for each column of the matrix. (Optional)
:param list(str) ylabels: Labels for each row of the matrix. (Optional)
:param list(float) camera: X,Y,Z coordinates indicating the camera position. The default value is ``(1,1,1)``.
:param str comment: A comment displayed with the plot, underneath the title.
:param dict extra_layout: optional dictionary for layout configuration, passed directly to plotly
See full details on the supported configuration: https://plotly.com/javascript/reference/surface/
example: extra_layout={'xaxis': {'type': 'date', 'range': ['2020-01-01', '2020-01-31']}}
"""
self._init_reporter()
if not isinstance(matrix, np.ndarray):
matrix = np.array(matrix)
return self._reporter.report_value_surface(
title=title,
series=series,
data=matrix.astype(np.float32),
iter=iteration or 0,
xlabels=xlabels,
ylabels=ylabels,
xtitle=xaxis,
ytitle=yaxis,
ztitle=zaxis,
camera=camera,
comment=comment,
layout_config=extra_layout
)
def publish(self):
# type: () -> ()
"""
@ -410,6 +1031,19 @@ class BaseModel(object):
if not self.published:
self._get_base_model().publish()
def _init_reporter(self):
if self._reporter:
return
metrics_manager = Metrics(
session=_Model._get_default_session(),
storage_uri=None,
task=self, # this is fine, the ID of the model will be fetched here
for_model=True
)
self._reporter = Reporter(metrics=metrics_manager, task=self, for_model=True)
def _running_remotely(self):
# type: () -> ()
return bool(running_remotely() and self._task is not None)

View File

@ -491,7 +491,7 @@ class BackgroundMonitor(object):
_at_exit = False
_instances = {} # type: Dict[int, List[BackgroundMonitor]]
def __init__(self, task, wait_period):
def __init__(self, task, wait_period, for_model=False):
self._event = ForkEvent()
self._done_ev = ForkEvent()
self._start_ev = ForkEvent()
@ -499,7 +499,7 @@ class BackgroundMonitor(object):
self._thread = None
self._thread_pid = None
self._wait_timeout = wait_period
self._subprocess = None if task.is_main_task() else False
self._subprocess = None if not for_model and task.is_main_task() else False
self._task_id = task.id
self._task_obj_id = id(task.id)