From d826e9806e5d4a14ad44e855da358098913ccad1 Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Fri, 26 Jul 2024 19:13:34 +0300 Subject: [PATCH 01/14] Fix injected task import in Task.populate() --- clearml/backend_interface/task/populate.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/clearml/backend_interface/task/populate.py b/clearml/backend_interface/task/populate.py index 35af6d77..76aaaab5 100644 --- a/clearml/backend_interface/task/populate.py +++ b/clearml/backend_interface/task/populate.py @@ -422,8 +422,9 @@ class CreateAndPopulate(object): "diff --git a{script_entry} b{script_entry}\n" \ "--- a{script_entry}\n" \ "+++ b{script_entry}\n" \ - "@@ -{idx_a},0 +{idx_b},3 @@\n" \ - "+from clearml import Task\n" \ + "@@ -{idx_a},0 +{idx_b},4 @@\n" \ + "+try: from allegroai import Task\n" \ + "+except ImportError: from clearml import Task\n" \ "+(__name__ != \"__main__\") or Task.init()\n" \ "+\n".format( script_entry=script_entry, idx_a=idx_a, idx_b=idx_a + 1) @@ -432,7 +433,11 @@ class CreateAndPopulate(object): pass elif local_entry_file and lines: # if we are here it means we do not have a git diff, but a single script file - init_lines = ["from clearml import Task\n", "(__name__ != \"__main__\") or Task.init()\n\n"] + init_lines = [ + "try: from allegroai import Task\n", + "except ImportError: from clearml import Task\n", + '(__name__ != "__main__") or Task.init()\n\n', + ] task_state['script']['diff'] = ''.join(lines[:idx_a] + init_lines + lines[idx_a:]) # no need to add anything, we patched it. task_init_patch = "" @@ -440,7 +445,8 @@ class CreateAndPopulate(object): # Add Task.init call # if we are here it means we do not have a git diff, but a single script file task_init_patch += \ - "from clearml import Task\n" \ + "try: from allegroai import Task\n" \ + "except ImportError: from clearml import Task\n" \ "(__name__ != \"__main__\") or Task.init()\n\n" task_state['script']['diff'] = task_init_patch + task_state['script'].get('diff', '') task_init_patch = "" From 2b66ee663ed5dd5fd81adf9e024e874159fb0a86 Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Mon, 29 Jul 2024 17:36:02 +0300 Subject: [PATCH 02/14] Fix dataset with external links will not reuse downloaded data from parents --- clearml/datasets/dataset.py | 105 +++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/clearml/datasets/dataset.py b/clearml/datasets/dataset.py index 4f7cf0db..ddf2e94e 100644 --- a/clearml/datasets/dataset.py +++ b/clearml/datasets/dataset.py @@ -123,6 +123,7 @@ class Dataset(object): __hyperparams_section = "Datasets" __datasets_runtime_prop = "datasets" __orig_datasets_runtime_prop_prefix = "orig_datasets" + __dataset_struct = "Dataset Struct" __preview_media_max_file_size = deferred_config("dataset.preview.media.max_file_size", 5 * 1024 * 1024, transform=int) __preview_tabular_table_count = deferred_config("dataset.preview.tabular.table_count", 10, transform=int) __preview_tabular_row_count = deferred_config("dataset.preview.tabular.row_count", 10, transform=int) @@ -2081,13 +2082,35 @@ class Dataset(object): self.update_changed_files(num_files_added=count - modified_count, num_files_modified=modified_count) return count - modified_count, modified_count + def _repair_dependency_graph(self): + """ + Repair dependency graph via the Dataset Struct configuration object. + Might happen for datasets with external files in old clearml versions + """ + try: + dataset_struct = self._task.get_configuration_object_as_dict(Dataset.__dataset_struct) + new_dependency_graph = {} + for dataset in dataset_struct.values(): + new_dependency_graph[dataset["job_id"]] = [dataset_struct[p]["job_id"] for p in dataset["parents"]] + self._dependency_graph = new_dependency_graph + except Exception as e: + LoggerRoot.get_base_logger().warning("Could not repair dependency graph. Error is: {}".format(e)) + def _update_dependency_graph(self): """ - Update the dependency graph based on the current self._dataset_file_entries state + Update the dependency graph based on the current self._dataset_file_entries + and self._dataset_link_entries states :return: """ # collect all dataset versions - used_dataset_versions = set(f.parent_dataset_id for f in self._dataset_file_entries.values()) + used_dataset_versions = set(f.parent_dataset_id for f in self._dataset_file_entries.values()) | set( + f.parent_dataset_id for f in self._dataset_link_entries.values() + ) + for dataset_id in used_dataset_versions: + if dataset_id not in self._dependency_graph and dataset_id != self._id: + self._repair_dependency_graph() + break + used_dataset_versions.add(self._id) current_parents = self._dependency_graph.get(self._id) or [] # remove parent versions we no longer need from the main version list @@ -2296,29 +2319,8 @@ class Dataset(object): Notice you should unlock it manually, or wait for the process to finish for auto unlocking. :param max_workers: Number of threads to be spawned when getting dataset files. Defaults to no multi-threading. """ - target_folder = ( - Path(target_folder) - if target_folder - else self._create_ds_target_folder( - lock_target_folder=lock_target_folder - )[0] - ).as_posix() - dependencies = self._get_dependencies_by_order( - include_unused=False, include_current=True - ) - links = {} - for dependency in dependencies: - ds = Dataset.get(dependency) - links.update(ds._dataset_link_entries) - links.update(self._dataset_link_entries) - def _download_link(link, target_path): if os.path.exists(target_path): - LoggerRoot.get_base_logger().info( - "{} already exists. Skipping downloading {}".format( - target_path, link - ) - ) return ok = False error = None @@ -2341,27 +2343,40 @@ class Dataset(object): LoggerRoot.get_base_logger().info(log_string) else: link.size = Path(target_path).stat().st_size - if not max_workers: - for relative_path, link in links.items(): - if not is_path_traversal(target_folder, relative_path): - target_path = os.path.join(target_folder, relative_path) - else: - LoggerRoot.get_base_logger().warning( - "Ignoring relative path `{}`: it must not traverse directories".format(relative_path) - ) - target_path = os.path.join(target_folder, os.path.basename(relative_path)) + + def _get_target_path(relative_path, target_folder): + if not is_path_traversal(target_folder, relative_path): + return os.path.join(target_folder, relative_path) + else: + LoggerRoot.get_base_logger().warning( + "Ignoring relative path `{}`: it must not traverse directories".format(relative_path) + ) + return os.path.join(target_folder, os.path.basename(relative_path)) + + def _submit_download_link(relative_path, link, target_folder, pool=None): + if link.parent_dataset_id != self.id: + return + target_path = _get_target_path(relative_path, target_folder) + if pool is None: _download_link(link, target_path) + else: + pool.submit(_download_link, link, target_path) + + target_folder = ( + Path(target_folder) + if target_folder + else self._create_ds_target_folder( + lock_target_folder=lock_target_folder + )[0] + ).as_posix() + + if not max_workers: + for relative_path, link in self._dataset_link_entries.items(): + _submit_download_link(relative_path, link, target_folder) else: with ThreadPoolExecutor(max_workers=max_workers) as pool: - for relative_path, link in links.items(): - if not is_path_traversal(target_folder, relative_path): - target_path = os.path.join(target_folder, relative_path) - else: - LoggerRoot.get_base_logger().warning( - "Ignoring relative path `{}`: it must not traverse directories".format(relative_path) - ) - target_path = os.path.join(target_folder, os.path.basename(relative_path)) - pool.submit(_download_link, link, target_path) + for relative_path, link in self._dataset_link_entries.items(): + _submit_download_link(relative_path, link, target_folder, pool=pool) def _extract_dataset_archive( self, @@ -2586,6 +2601,7 @@ class Dataset(object): :param include_current: If True include the current dataset ID as the last ID in the list :return: list of str representing the datasets id """ + self._update_dependency_graph() roots = [self._id] dependencies = [] # noinspection DuplicatedCode @@ -3027,7 +3043,7 @@ class Dataset(object): # fetch the parents of this version (task) based on what we have on the Task itself. # noinspection PyBroadException try: - dataset_version_node = task.get_configuration_object_as_dict("Dataset Struct") + dataset_version_node = task.get_configuration_object_as_dict(Dataset.__dataset_struct) # fine the one that is us for node in dataset_version_node.values(): if node["job_id"] != id_: @@ -3056,7 +3072,7 @@ class Dataset(object): dataset_struct[indices[id_]]["parents"] = [indices[p] for p in parents] # noinspection PyProtectedMember self._task._set_configuration( - name="Dataset Struct", + name=Dataset.__dataset_struct, description="Structure of the dataset", config_type="json", config_text=json.dumps(dataset_struct, indent=2), @@ -3234,7 +3250,8 @@ class Dataset(object): return None - errors = pool.map(copy_file, self._dataset_file_entries.values()) + errors = list(pool.map(copy_file, self._dataset_file_entries.values())) + errors.extend(list(pool.map(copy_file, self._dataset_link_entries.values()))) CacheManager.get_cache_manager(cache_context=self.__cache_context).unlock_cache_folder( ds_base_folder.as_posix()) From 1b474dc0b057b69c76bc2daa9eb8be927cb25efa Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Mon, 29 Jul 2024 17:37:05 +0300 Subject: [PATCH 03/14] Fix hierarchy for pipeline nodes without args --- clearml/automation/controller.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clearml/automation/controller.py b/clearml/automation/controller.py index 8d14c17c..62d26781 100644 --- a/clearml/automation/controller.py +++ b/clearml/automation/controller.py @@ -4805,11 +4805,11 @@ class PipelineDecorator(PipelineController): if n not in _node.parents: _node.parents.append(n) break - if kwargs: - leaves = cls._singleton._find_executed_node_leaves() - _node.parents = (_node.parents or []) + [ - x for x in cls._evaluated_return_values.get(tid, []) if x in leaves - ] + + leaves = cls._singleton._find_executed_node_leaves() + _node.parents = (_node.parents or []) + [ + x for x in cls._evaluated_return_values.get(tid, []) if x in leaves + ] if not cls._singleton._abort_running_steps_on_failure: for parent in _node.parents: From 462b01ee0e0ac9e496ac9bbba8c2943636a82247 Mon Sep 17 00:00:00 2001 From: pollfly <75068813+pollfly@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:32:08 +0300 Subject: [PATCH 04/14] Edit Logger docs (#1302) --- docs/logger.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/logger.md b/docs/logger.md index 59f8ea9a..fbe0360a 100644 --- a/docs/logger.md +++ b/docs/logger.md @@ -22,13 +22,13 @@ Using the **ClearML** [Logger](https://github.com/allegroai/clearml/blob/master/ Additionally, the **ClearML** Logger module provides methods that allow you to do the following: * Get the [current logger]() - * Overrride the ClearML configuration file with a [default upload destination]() for images and files + * Override the ClearML configuration file with a [default upload destination]() for images and files ## Graphs and Images ### Scalar Metrics -Use to report scalar metrics by iteration as a line plot. +Report scalar metrics by iteration as a line plot. First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/scalar_reporting.py)) with the following method. @@ -99,7 +99,7 @@ def report_scalar(self, title, series, value, iteration) ### Histograms -Use to report any data by iteration as a histogram. +Report any data by iteration as a histogram. First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/scatter_hist_confusion_mat_reporting.py)) with the following method. @@ -197,7 +197,7 @@ def report_histogram(self, title, series, values, iteration, labels=None, xlabel ### Line Plots -Use to report any data by iteration as a single or multiple line plot. +Report any data by iteration as a single or multiple line plot. First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/scatter_hist_confusion_mat_reporting.py)) with the following method. @@ -323,7 +323,7 @@ def report_line_plot(self, title, series, iteration, xaxis, yaxis, mode='lines', ### 2D Scatter Diagrams -Use to report any vector data as a 2D scatter diagram. +Report any vector data as a 2D scatter diagram. First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/scatter_hist_confusion_mat_reporting.py)) with the following method. @@ -459,7 +459,7 @@ def report_scatter2d(self, title, series, scatter, iteration, xaxis=None, yaxis= ### 3D Scatter Diagrams -Use to report any array data as a 3D scatter diagram. +Report any array data as a 3D scatter diagram. First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/3d_plots_reporting.py)) with the following method. @@ -584,7 +584,7 @@ def report_scatter3d(self, title, series, scatter, iteration, labels=None, mode=
  • lines+markers
  • - The default values is lines. + The default value is lines. No @@ -595,7 +595,7 @@ def report_scatter3d(self, title, series, scatter, iteration, labels=None, mode= ### Confusion Matrices -Use to report a heat-map matrix as a confusion matrix. You can also plot a heat-map as a [surface diagram](#surface-diagrams). +Report a heat-map matrix as a confusion matrix. You can also plot a heat-map as a [surface diagram](#surface-diagrams). First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/scatter_hist_confusion_mat_reporting.py)) with the following method. @@ -687,7 +687,7 @@ def report_confusion_matrix(self, title, series, matrix, iteration, xlabels=None ### Surface Diagrams -Use to plot a heat-map matrix as a surface diagram. You can also plot a heat-map as a [confusion matrix](#confusion-matrices). +Plot a heat-map matrix as a surface diagram. You can also plot a heat-map as a [confusion matrix](#confusion-matrices). First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/reporting/3d_plots_reporting.py)) with the following method. @@ -818,7 +818,7 @@ def report_surface(self, title, series, matrix, iteration, xlabels=None, ylabels ### Images -Use to report an image and upload its contents to the bucket specified in the **ClearML** configuration file, +Report an image and upload its contents to the bucket specified in the **ClearML** configuration file, or a [default upload destination](#set-default-upload-destination), if you set a default. First [get the current logger](#get-the-current-logger) and then use it (see an [example script](https://github.com/allegroai/clearml/blob/master/examples/manual_reporting.py)) with the following method. @@ -896,7 +896,7 @@ def report_image(self, title, series, iteration, local_path=None, matrix=None, m ndarray - A 3D numpy.ndarray object containing image data (RGB). If path is not specified, then matrix is required. The default values is None. + A 3D numpy.ndarray object containing image data (RGB). If path is not specified, then matrix is required. The default value is None. No @@ -917,7 +917,7 @@ def report_image(self, title, series, iteration, local_path=None, matrix=None, m string - The path of the image file. If matrix is not specified, then path is required. The default values is None. + The path of the image file. If matrix is not specified, then path is required. The default value is None. No @@ -948,13 +948,13 @@ By setting the `CLEARML_LOG_ENVIRONMENT` environment variable, make **ClearML** * All environment variables - export CLEARML_LOG_ENVIRONMENT="*" + export CLEARML_LOG_ENVIRONMENT=* * Specific environment variables For example, log `PWD` and `PYTHONPATH` - export CLEARML_LOG_ENVIRONMENT="PWD,PYTHONPATH" + export CLEARML_LOG_ENVIRONMENT=PWD,PYTHONPATH * No environment variables @@ -1368,7 +1368,7 @@ None. ### Set Default Upload Destination -Use to specify the default destination storage location used for uploading images. +Specify the default destination storage location used for uploading images. Images are uploaded and a link to the image is reported. Credentials for the storage location are in the global configuration file (for example, on Linux, ~/clearml.conf). From a7b58900283c5aada3a0d7906f1a19cbe07f643b Mon Sep 17 00:00:00 2001 From: Rafael Tvelov Date: Wed, 31 Jul 2024 14:19:31 +0300 Subject: [PATCH 05/14] Fix scalar logging bug with Fire (#1301) Fixes `_patched_call` using under Fire scope, see #1300 --- clearml/binding/fire_bind.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clearml/binding/fire_bind.py b/clearml/binding/fire_bind.py index 0953d229..173c5420 100644 --- a/clearml/binding/fire_bind.py +++ b/clearml/binding/fire_bind.py @@ -6,7 +6,7 @@ except ImportError: fire = None import inspect -from .frameworks import _patched_call # noqa +from .frameworks import _patched_call_no_recursion_guard # noqa from ..config import get_remote_task_id, running_remotely from ..utilities.dicts import cast_str_to_bool @@ -57,9 +57,9 @@ class PatchFire: if not cls.__patched: cls.__patched = True if running_remotely(): - fire.core._Fire = _patched_call(fire.core._Fire, PatchFire.__Fire) + fire.core._Fire = _patched_call_no_recursion_guard(fire.core._Fire, PatchFire.__Fire) else: - fire.core._CallAndUpdateTrace = _patched_call( + fire.core._CallAndUpdateTrace = _patched_call_no_recursion_guard( fire.core._CallAndUpdateTrace, PatchFire.__CallAndUpdateTrace ) From d8ca5c73c4749e91d6a5dd39b7d1fb4e9ccee5af Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Wed, 31 Jul 2024 17:24:05 +0300 Subject: [PATCH 06/14] Flake8 --- clearml/backend_interface/task/task.py | 4 ++-- clearml/external/kerastuner.py | 6 ++---- clearml/task.py | 4 ++-- examples/reporting/requirements.txt | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/clearml/backend_interface/task/task.py b/clearml/backend_interface/task/task.py index 8298c77c..380e5171 100644 --- a/clearml/backend_interface/task/task.py +++ b/clearml/backend_interface/task/task.py @@ -94,7 +94,7 @@ class Task(IdObjectBase, AccessMixin, SetupUploadMixin): return str(self) == str(other) def __repr__(self): - return f"TaskTypes.{self.value}" + return "TaskTypes.{}".format(self.value) training = 'training' testing = 'testing' @@ -116,7 +116,7 @@ class Task(IdObjectBase, AccessMixin, SetupUploadMixin): return str(self) == str(other) def __repr__(self): - return f"TaskTypes.{self.value}" + return "TaskTypes.{}".format(self.value) created = "created" queued = "queued" diff --git a/clearml/external/kerastuner.py b/clearml/external/kerastuner.py index eb411ab9..e03b1568 100644 --- a/clearml/external/kerastuner.py +++ b/clearml/external/kerastuner.py @@ -1,12 +1,10 @@ from typing import Optional from logging import getLogger +from ..task import Task _logger = getLogger("clearml.external.kerastuner") -from ..task import Task - - try: import pandas as pd except ImportError: @@ -119,4 +117,4 @@ else: summary = pd.concat([summary, pd.DataFrame(trial_dict, index=[trial.trial_id])], ignore_index=True) summary.index.name = "trial id" summary = summary[["trial id", *sorted(summary.columns[1:])]] - self.task.get_logger().report_table("summary", "trial", 0, table_plot=summary) \ No newline at end of file + self.task.get_logger().report_table("summary", "trial", 0, table_plot=summary) diff --git a/clearml/task.py b/clearml/task.py index 4a92ce57..76b98630 100644 --- a/clearml/task.py +++ b/clearml/task.py @@ -180,8 +180,8 @@ class Task(_Task): __detect_repo_async = deferred_config('development.vcs_repo_detect_async', False) __default_output_uri = DEV_DEFAULT_OUTPUT_URI.get() or deferred_config('development.default_output_uri', None) - __hidden_tag = "hidden" - + __hidden_tag = "hidden" + _launch_multi_node_section = "launch_multi_node" _launch_multi_node_instance_tag = "multi_node_instance" diff --git a/examples/reporting/requirements.txt b/examples/reporting/requirements.txt index bb9393a3..ace399d2 100644 --- a/examples/reporting/requirements.txt +++ b/examples/reporting/requirements.txt @@ -3,6 +3,7 @@ clearml>=1.14.4 matplotlib >= 3.1.1 ; python_version >= '3.6' matplotlib >= 2.2.4 ; python_version < '3.6' numpy != 1.24.0 # https://github.com/numpy/numpy/issues/22826 +bokeh_sampledata==2024.2 ; python_version >= '3.10' pandas pillow>=4.0 plotly From b298e212dde631f3b2326e0a3a519d0235510cbb Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Wed, 31 Jul 2024 17:24:43 +0300 Subject: [PATCH 07/14] Add docstring allowing users to pass packages=False to revert to requirements.txt inside their git repository --- clearml/automation/controller.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/clearml/automation/controller.py b/clearml/automation/controller.py index 62d26781..0b932796 100644 --- a/clearml/automation/controller.py +++ b/clearml/automation/controller.py @@ -212,7 +212,7 @@ class PipelineController(object): docker=None, # type: Optional[str] docker_args=None, # type: Optional[str] docker_bash_setup_script=None, # type: Optional[str] - packages=None, # type: Optional[Union[str, Sequence[str]]] + packages=None, # type: Optional[Union[bool, str, Sequence[str]]] repo=None, # type: Optional[str] repo_branch=None, # type: Optional[str] repo_commit=None, # type: Optional[str] @@ -273,6 +273,7 @@ class PipelineController(object): :param packages: Manually specify a list of required packages or a local requirements.txt file. Example: ["tqdm>=2.1", "scikit-learn"] or "./requirements.txt" If not provided, packages are automatically added. + Use `False` to install requirements from "requirements.txt" inside your git repository :param repo: Optional, specify a repository to attach to the pipeline controller, when remotely executing. Allow users to execute the controller inside the specified repository, enabling them to load modules/script from the repository. Notice the execution work directory will be the repository root folder. @@ -711,7 +712,7 @@ class PipelineController(object): task_type=None, # type: Optional[str] auto_connect_frameworks=None, # type: Optional[dict] auto_connect_arg_parser=None, # type: Optional[dict] - packages=None, # type: Optional[Union[str, Sequence[str]]] + packages=None, # type: Optional[Union[bool, str, Sequence[str]]] repo=None, # type: Optional[str] repo_branch=None, # type: Optional[str] repo_commit=None, # type: Optional[str] @@ -786,6 +787,7 @@ class PipelineController(object): :param packages: Manually specify a list of required packages or a local requirements.txt file. Example: ["tqdm>=2.1", "scikit-learn"] or "./requirements.txt" If not provided, packages are automatically added based on the imports used in the function. + Use `False` to install requirements from "requirements.txt" inside your git repository :param repo: Optional, specify a repository to attach to the function, when remotely executing. Allow users to execute the function inside the specified repository, enabling to load modules/script from a repository Notice the execution work directory will be the repository root folder. @@ -2064,7 +2066,7 @@ class PipelineController(object): task_type=None, # type: Optional[str] auto_connect_frameworks=None, # type: Optional[dict] auto_connect_arg_parser=None, # type: Optional[dict] - packages=None, # type: Optional[Union[str, Sequence[str]]] + packages=None, # type: Optional[Union[bool, str, Sequence[str]]] repo=None, # type: Optional[str] repo_branch=None, # type: Optional[str] repo_commit=None, # type: Optional[str] @@ -2139,6 +2141,7 @@ class PipelineController(object): :param packages: Manually specify a list of required packages or a local requirements.txt file. Example: ["tqdm>=2.1", "scikit-learn"] or "./requirements.txt" If not provided, packages are automatically added based on the imports used in the function. + Use `False` to install requirements from "requirements.txt" inside your git repository :param repo: Optional, specify a repository to attach to the function, when remotely executing. Allow users to execute the function inside the specified repository, enabling to load modules/script from a repository Notice the execution work directory will be the repository root folder. @@ -3485,7 +3488,7 @@ class PipelineDecorator(PipelineController): docker=None, # type: Optional[str] docker_args=None, # type: Optional[str] docker_bash_setup_script=None, # type: Optional[str] - packages=None, # type: Optional[Union[str, Sequence[str]]] + packages=None, # type: Optional[Union[bool, str, Sequence[str]]] repo=None, # type: Optional[str] repo_branch=None, # type: Optional[str] repo_commit=None, # type: Optional[str] @@ -3541,6 +3544,7 @@ class PipelineDecorator(PipelineController): :param packages: Manually specify a list of required packages or a local requirements.txt file. Example: ["tqdm>=2.1", "scikit-learn"] or "./requirements.txt" If not provided, packages are automatically added. + Use `False` to install requirements from "requirements.txt" inside your git repository :param repo: Optional, specify a repository to attach to the pipeline controller, when remotely executing. Allow users to execute the controller inside the specified repository, enabling them to load modules/script from the repository. Notice the execution work directory will be the repository root folder. @@ -3950,7 +3954,7 @@ class PipelineDecorator(PipelineController): return_values=('return_object', ), # type: Union[str, Sequence[str]] name=None, # type: Optional[str] cache=False, # type: bool - packages=None, # type: Optional[Union[str, Sequence[str]]] + packages=None, # type: Optional[Union[bool, str, Sequence[str]]] parents=None, # type: Optional[List[str]] execution_queue=None, # type: Optional[str] continue_on_fail=False, # type: bool @@ -3992,6 +3996,7 @@ class PipelineDecorator(PipelineController): :param packages: Manually specify a list of required packages or a local requirements.txt file. Example: ["tqdm>=2.1", "scikit-learn"] or "./requirements.txt" If not provided, packages are automatically added based on the imports used inside the wrapped function. + Use `False` to install requirements from "requirements.txt" inside your git repository :param parents: Optional list of parent nodes in the DAG. The current step in the pipeline will be sent for execution only after all the parent nodes have been executed successfully. @@ -4415,7 +4420,7 @@ class PipelineDecorator(PipelineController): docker=None, # type: Optional[str] docker_args=None, # type: Optional[str] docker_bash_setup_script=None, # type: Optional[str] - packages=None, # type: Optional[Union[str, Sequence[str]]] + packages=None, # type: Optional[Union[bool, str, Sequence[str]]] repo=None, # type: Optional[str] repo_branch=None, # type: Optional[str] repo_commit=None, # type: Optional[str] @@ -4502,6 +4507,7 @@ class PipelineDecorator(PipelineController): :param packages: Manually specify a list of required packages or a local requirements.txt file. Example: ["tqdm>=2.1", "scikit-learn"] or "./requirements.txt" If not provided, packages are automatically added based on the imports used in the function. + Use `False` to install requirements from "requirements.txt" inside your git repository :param repo: Optional, specify a repository to attach to the function, when remotely executing. Allow users to execute the function inside the specified repository, enabling them to load modules/script from the repository. Notice the execution work directory will be the repository root folder. From 62dd92a22d627407c8ebeb62a53c69e845d12cd8 Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Wed, 31 Jul 2024 17:25:12 +0300 Subject: [PATCH 08/14] Fix when abort callback is set, set task status to stopped only if running locally, otherwise leave it for the Agent to set it. --- clearml/task.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clearml/task.py b/clearml/task.py index 76b98630..d63a2c78 100644 --- a/clearml/task.py +++ b/clearml/task.py @@ -4148,7 +4148,10 @@ class Task(_Task): ) ) self.flush(wait_for_uploads=True) - self.stopped(status_reason='USER ABORTED') + + # if running remotely, we want the daemon to kill us + if self.running_locally(): + self.stopped(status_reason='USER ABORTED') if self._dev_worker: self._dev_worker.unregister() From 87233dfeac43595482d92257efb6da8802ae9137 Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Wed, 31 Jul 2024 17:26:04 +0300 Subject: [PATCH 09/14] Protect against jsonschema / referencing import to include TypeError --- clearml/backend_api/session/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clearml/backend_api/session/request.py b/clearml/backend_api/session/request.py index 9004127a..99c9ec25 100644 --- a/clearml/backend_api/session/request.py +++ b/clearml/backend_api/session/request.py @@ -7,7 +7,7 @@ try: # Since `referencing`` only supports Python >= 3.8, this try-except blocks maintain support # for earlier python versions. from referencing.exceptions import Unresolvable -except ImportError: +except (ImportError, TypeError): from jsonschema.exceptions import RefResolutionError as Unresolvable from .apimodel import ApiModel From 165e8a07f920d1baf272c8ed3728ae7a8916c382 Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Wed, 31 Jul 2024 17:27:22 +0300 Subject: [PATCH 10/14] Fix offline behavior --- clearml/datasets/dataset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/clearml/datasets/dataset.py b/clearml/datasets/dataset.py index ddf2e94e..4ac0e001 100644 --- a/clearml/datasets/dataset.py +++ b/clearml/datasets/dataset.py @@ -2354,7 +2354,7 @@ class Dataset(object): return os.path.join(target_folder, os.path.basename(relative_path)) def _submit_download_link(relative_path, link, target_folder, pool=None): - if link.parent_dataset_id != self.id: + if link.parent_dataset_id != self.id and not link.parent_dataset_id.startswith("offline-"): return target_path = _get_target_path(relative_path, target_folder) if pool is None: @@ -2758,6 +2758,13 @@ class Dataset(object): for k, v in dataset._dependency_graph.items() # noqa } # noinspection PyProtectedMember + for entry in dataset._dataset_file_entries.values(): + if entry.parent_dataset_id.startswith("offline-"): + entry.parent_dataset_id = id + for entry in dataset._dataset_link_entries.values(): + if entry.parent_dataset_id.startswith("offline-"): + entry.parent_dataset_id = id + # noinspection PyProtectedMember dataset._update_dependency_graph() # noinspection PyProtectedMember dataset._log_dataset_page() From 386e1874bef8783c2c50a30b9fdde8ae08dea76d Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Wed, 31 Jul 2024 17:28:25 +0300 Subject: [PATCH 11/14] Add retries parameter to StorageManager.upload_folder (#1305) --- clearml/storage/manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/clearml/storage/manager.py b/clearml/storage/manager.py index a4fb8d33..9c35dea6 100644 --- a/clearml/storage/manager.py +++ b/clearml/storage/manager.py @@ -211,8 +211,8 @@ class StorageManager(object): return Session.get_files_server_host() @classmethod - def upload_folder(cls, local_folder, remote_url, match_wildcard=None): - # type: (str, str, Optional[str]) -> Optional[str] + def upload_folder(cls, local_folder, remote_url, match_wildcard=None, retries=None): + # type: (str, str, Optional[str], Optional[int]) -> Optional[str] """ Upload local folder recursively to a remote storage, maintaining the sub folder structure in the remote storage. @@ -231,6 +231,7 @@ class StorageManager(object): Example: `*.json` Notice: target file size/date are not checked. Default True, always upload. Notice if uploading to http, we will always overwrite the target. + :param int retries: Number of retries before failing to upload a file in the folder. :return: Newly uploaded remote URL or None on error. """ @@ -250,6 +251,7 @@ class StorageManager(object): pool.apply_async( helper.upload, args=(str(path), str(path).replace(local_folder, remote_url)), + kwds={"retries": retries if retries else cls._file_upload_retries} ) ) From 028adb45fb636228c898bd8b27aa3c01e9bd170b Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Fri, 2 Aug 2024 15:21:31 +0300 Subject: [PATCH 12/14] Black formatting --- clearml/cli/config/__main__.py | 248 ++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 113 deletions(-) diff --git a/clearml/cli/config/__main__.py b/clearml/cli/config/__main__.py index 46a4ccb9..9b088093 100644 --- a/clearml/cli/config/__main__.py +++ b/clearml/cli/config/__main__.py @@ -15,13 +15,15 @@ from clearml.backend_config.defs import LOCAL_CONFIG_FILES, LOCAL_CONFIG_FILE_OV from clearml.config import config_obj from clearml.utilities.pyhocon import ConfigFactory, ConfigMissingException -description = "\n" \ - "Please create new clearml credentials through the settings page in " \ - "your `clearml-server` web app (e.g. http://localhost:8080//settings/workspace-configuration) \n"\ - "Or create a free account at https://app.clear.ml/settings/workspace-configuration\n\n" \ - "In settings page, press \"Create new credentials\", then press \"Copy to clipboard\".\n" \ - "\n" \ +description = ( + "\n" + "Please create new clearml credentials through the settings page in " + "your `clearml-server` web app (e.g. http://localhost:8080//settings/workspace-configuration) \n" + "Or create a free account at https://app.clear.ml/settings/workspace-configuration\n\n" + 'In settings page, press "Create new credentials", then press "Copy to clipboard".\n' + "\n" "Paste copied configuration here:\n" +) host_description = """ Editing configuration file: {CONFIG_FILE} @@ -30,9 +32,9 @@ Enter the url of the clearml-server's Web service, for example: {HOST} # noinspection PyBroadException try: - def_host = ENV_HOST.get(default=config_obj.get("api.web_server")) or 'http://localhost:8080' + def_host = ENV_HOST.get(default=config_obj.get("api.web_server")) or "http://localhost:8080" except Exception: - def_host = 'http://localhost:8080' + def_host = "http://localhost:8080" def validate_file(string): @@ -51,35 +53,38 @@ def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument( - "--file", "-F", help="Target configuration file path (default is %(default)s)", + "--file", + "-F", + help="Target configuration file path (default is %(default)s)", default=default_config_file, - type=validate_file + type=validate_file, ) args = p.parse_args() - print('ClearML SDK setup process') + print("ClearML SDK setup process") conf_file = Path(os.path.expanduser(args.file)).absolute() if conf_file.exists() and conf_file.is_file() and conf_file.stat().st_size > 0: - print('Configuration file already exists: {}'.format(str(conf_file))) - print('Leaving setup, feel free to edit the configuration file.') + print("Configuration file already exists: {}".format(str(conf_file))) + print("Leaving setup, feel free to edit the configuration file.") return - print(description, end='') - sentinel = '' - parse_input = '' + print(description, end="") + sentinel = "" + parse_input = "" if os.environ.get("JPY_PARENT_PID"): # When running from a colab instance and calling clearml-init # colab will squish the api credentials into a single line # The regex splits this single line based on 2 spaces or more import re + api_input = input() - parse_input = '\n'.join(re.split(r" {2,}", api_input)) + parse_input = "\n".join(re.split(r" {2,}", api_input)) else: for line in iter(input, sentinel): - parse_input += line+'\n' - if line.rstrip() == '}': + parse_input += line + "\n" + if line.rstrip() == "}": break credentials = None @@ -102,11 +107,14 @@ def main(): files_server = files_server or None while not credentials or set(credentials) != {"access_key", "secret_key"}: - print('Could not parse credentials, please try entering them manually.') + print("Could not parse credentials, please try entering them manually.") credentials = read_manual_credentials() - print('Detected credentials key=\"{}\" secret=\"{}\"'.format(credentials['access_key'], - credentials['secret_key'][0:4] + "***")) + print( + 'Detected credentials key="{}" secret="{}"'.format( + credentials["access_key"], credentials["secret_key"][0:4] + "***" + ) + ) web_input = True if web_server: host = web_server @@ -114,47 +122,43 @@ def main(): web_input = False host = api_server else: - print(host_description.format(CONFIG_FILE=args.file, HOST=def_host,)) - host = input_url('WEB Host', '') + print( + host_description.format( + CONFIG_FILE=args.file, + HOST=def_host, + ) + ) + host = input_url("WEB Host", "") parsed_host = verify_url(host) api_host, files_host, web_host = parse_known_host(parsed_host) - hosts_dict = { - "API": api_server, - "Files": files_server, - "Web": web_server - } + hosts_dict = {"API": api_server, "Files": files_server, "Web": web_server} - infered_hosts_dict = { - "API": api_host, - "Files": files_host, - "Web": web_host - } + infered_hosts_dict = {"API": api_host, "Files": files_host, "Web": web_host} for host_type, url in six.iteritems(hosts_dict): - if url is None or not ( - url.startswith('http://') or url.startswith('https://') - ): + if url is None or not (url.startswith("http://") or url.startswith("https://")): infered_host_url = infered_hosts_dict[host_type] if infered_host_url != "": hosts_dict[host_type] = infered_host_url else: hosts_dict[host_type] = input_url(host_type) - api_host, files_host, web_host = hosts_dict['API'], hosts_dict['Files'], hosts_dict['Web'] + api_host, files_host, web_host = hosts_dict["API"], hosts_dict["Files"], hosts_dict["Web"] # one of these two we configured if not web_input: - web_host = input_url('Web Application Host', web_host) + web_host = input_url("Web Application Host", web_host) else: if web_input is True and not web_host: web_host = host - print('\nClearML Hosts configuration:\nWeb App: {}\nAPI: {}\nFile Store: {}\n'.format( - web_host, api_host, files_host)) + print( + "\nClearML Hosts configuration:\nWeb App: {}\nAPI: {}\nFile Store: {}\n".format(web_host, api_host, files_host) + ) - if len(set([web_host, api_host, files_host])) != 3: + if len({web_host, api_host, files_host}) != 3: raise ValueError("All three server URLs should be distinct") retry = 1 @@ -166,88 +170,94 @@ def main(): if retry < max_retries + 1: credentials = read_manual_credentials() else: - print('Exiting setup without creating configuration file') + print("Exiting setup without creating configuration file") return # noinspection PyBroadException try: - default_sdk_conf = Path(__file__).absolute().parents[2] / 'config/default/sdk.conf' - with open(str(default_sdk_conf), 'rt') as f: + default_sdk_conf = Path(__file__).absolute().parents[2] / "config/default/sdk.conf" + with open(str(default_sdk_conf), "rt") as f: default_sdk = f.read() except Exception: - print('Error! Could not read default configuration file') + print("Error! Could not read default configuration file") return # noinspection PyBroadException try: - with open(str(conf_file), 'wt') as f: - header = '# ClearML SDK configuration file\n' \ - 'api {\n' \ - ' # Notice: \'host\' is the api server (default port 8008), not the web server.\n' \ - ' api_server: %s\n' \ - ' web_server: %s\n' \ - ' files_server: %s\n' \ - ' # Credentials are generated using the webapp, %s/settings\n' \ - ' # Override with os environment: CLEARML_API_ACCESS_KEY / CLEARML_API_SECRET_KEY\n' \ - ' credentials {"access_key": "%s", "secret_key": "%s"}\n' \ - '}\n' \ - 'sdk ' % (api_host, web_host, files_host, - web_host, credentials['access_key'], credentials['secret_key']) + with open(str(conf_file), "wt") as f: + header = ( + "# ClearML SDK configuration file\n" + "api {\n" + " # Notice: 'host' is the api server (default port 8008), not the web server.\n" + " api_server: %s\n" + " web_server: %s\n" + " files_server: %s\n" + " # Credentials are generated using the webapp, %s/settings\n" + " # Override with os environment: CLEARML_API_ACCESS_KEY / CLEARML_API_SECRET_KEY\n" + ' credentials {"access_key": "%s", "secret_key": "%s"}\n' + "}\n" + "sdk " + % (api_host, web_host, files_host, web_host, credentials["access_key"], credentials["secret_key"]) + ) f.write(header) f.write(default_sdk) except Exception: - print('Error! Could not write configuration file at: {}'.format(str(conf_file))) + print("Error! Could not write configuration file at: {}".format(str(conf_file))) return - print('\nNew configuration stored in {}'.format(str(conf_file))) - print('ClearML setup completed successfully.') + print("\nNew configuration stored in {}".format(str(conf_file))) + print("ClearML setup completed successfully.") def parse_known_host(parsed_host): - if parsed_host.netloc.startswith('demoapp.'): + if parsed_host.netloc.startswith("demoapp."): # this is our demo server - api_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('demoapp.', 'demoapi.', 1) + parsed_host.path + api_host = parsed_host.scheme + "://" + parsed_host.netloc.replace("demoapp.", "demoapi.", 1) + parsed_host.path web_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('demoapp.', 'demofiles.', - 1) + parsed_host.path - elif parsed_host.netloc.startswith('app.'): + files_host = ( + parsed_host.scheme + "://" + parsed_host.netloc.replace("demoapp.", "demofiles.", 1) + parsed_host.path + ) + elif parsed_host.netloc.startswith("app."): # this is our application server - api_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('app.', 'api.', 1) + parsed_host.path + api_host = parsed_host.scheme + "://" + parsed_host.netloc.replace("app.", "api.", 1) + parsed_host.path web_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('app.', 'files.', 1) + parsed_host.path - elif parsed_host.netloc.startswith('demoapi.'): - print('{} is the api server, we need the web server. Replacing \'demoapi.\' with \'demoapp.\''.format( - parsed_host.netloc)) + files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace("app.", "files.", 1) + parsed_host.path + elif parsed_host.netloc.startswith("demoapi."): + print( + "{} is the api server, we need the web server. Replacing 'demoapi.' with 'demoapp.'".format( + parsed_host.netloc + ) + ) api_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - web_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('demoapi.', 'demoapp.', 1) + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('demoapi.', 'demofiles.', - 1) + parsed_host.path - elif parsed_host.netloc.startswith('api.'): - print('{} is the api server, we need the web server. Replacing \'api.\' with \'app.\''.format( - parsed_host.netloc)) + web_host = parsed_host.scheme + "://" + parsed_host.netloc.replace("demoapi.", "demoapp.", 1) + parsed_host.path + files_host = ( + parsed_host.scheme + "://" + parsed_host.netloc.replace("demoapi.", "demofiles.", 1) + parsed_host.path + ) + elif parsed_host.netloc.startswith("api."): + print("{} is the api server, we need the web server. Replacing 'api.' with 'app.'".format(parsed_host.netloc)) api_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - web_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('api.', 'app.', 1) + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace('api.', 'files.', 1) + parsed_host.path + web_host = parsed_host.scheme + "://" + parsed_host.netloc.replace("api.", "app.", 1) + parsed_host.path + files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace("api.", "files.", 1) + parsed_host.path elif parsed_host.port == 8008: - print('Port 8008 is the api port. Replacing 8008 with 8080 for Web application') + print("Port 8008 is the api port. Replacing 8008 with 8080 for Web application") api_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - web_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(':8008', ':8080', 1) + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(':8008', ':8081', 1) + parsed_host.path + web_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(":8008", ":8080", 1) + parsed_host.path + files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(":8008", ":8081", 1) + parsed_host.path elif parsed_host.port == 8080: - print('Port 8080 is the web port. Replacing 8080 with 8008 for API server') - api_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(':8080', ':8008', 1) + parsed_host.path + print("Port 8080 is the web port. Replacing 8080 with 8008 for API server") + api_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(":8080", ":8008", 1) + parsed_host.path web_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(':8080', ':8081', 1) + parsed_host.path + files_host = parsed_host.scheme + "://" + parsed_host.netloc.replace(":8080", ":8081", 1) + parsed_host.path elif parsed_host.port is None: - print('Web app hosted on standard port using ' + parsed_host.scheme + ' protocol.') - print('Assuming files and api ports are unchanged and use the same (' + parsed_host.scheme + ') protocol') - api_host = parsed_host.scheme + "://" + parsed_host.netloc + ':8008' + parsed_host.path + print("Web app hosted on standard port using " + parsed_host.scheme + " protocol.") + print("Assuming files and api ports are unchanged and use the same (" + parsed_host.scheme + ") protocol") + api_host = parsed_host.scheme + "://" + parsed_host.netloc + ":8008" + parsed_host.path web_host = parsed_host.scheme + "://" + parsed_host.netloc + parsed_host.path - files_host = parsed_host.scheme + "://" + parsed_host.netloc + ':8081' + parsed_host.path + files_host = parsed_host.scheme + "://" + parsed_host.netloc + ":8081" + parsed_host.path else: print("Warning! Could not parse host name") - api_host = '' - web_host = '' - files_host = '' + api_host = "" + web_host = "" + files_host = "" return api_host, files_host, web_host @@ -256,18 +266,25 @@ def verify_credentials(api_host, credentials): """check if the credentials are valid""" # noinspection PyBroadException try: - print('Verifying credentials ...') + print("Verifying credentials ...") if api_host: - Session(api_key=credentials['access_key'], secret_key=credentials['secret_key'], host=api_host, - http_retries_config={"total": 2}) - print('Credentials verified!') + Session( + api_key=credentials["access_key"], + secret_key=credentials["secret_key"], + host=api_host, + http_retries_config={"total": 2}, + ) + print("Credentials verified!") return True else: print("Can't verify credentials") return False except Exception: - print('Error: could not verify credentials: key={} secret={}'.format( - credentials.get('access_key'), credentials.get('secret_key'))) + print( + "Error: could not verify credentials: key={} secret={}".format( + credentials.get("access_key"), credentials.get("secret_key") + ) + ) return False @@ -292,18 +309,18 @@ def get_parsed_field(parsed_config, fields): def read_manual_credentials(): - print('Enter user access key: ', end='') + print("Enter user access key: ", end="") access_key = input() - print('Enter user secret: ', end='') + print("Enter user secret: ", end="") secret_key = input() return {"access_key": access_key, "secret_key": secret_key} def input_url(host_type, host=None): while True: - print('{} configured to: {}'.format(host_type, '[{}] '.format(host) if host else ''), end='') + print("{} configured to: {}".format(host_type, "[{}] ".format(host) if host else ""), end="") parse_input = input() - if host and (not parse_input or parse_input.lower() == 'yes' or parse_input.lower() == 'y'): + if host and (not parse_input or parse_input.lower() == "yes" or parse_input.lower() == "y"): break parsed_host = verify_url(parse_input) if parse_input else None if parse_input and parsed_host: @@ -313,29 +330,34 @@ def input_url(host_type, host=None): def input_host_port(host_type, parsed_host): - print('Enter port for {} host '.format(host_type), end='') + print("Enter port for {} host ".format(host_type), end="") replace_port = input().lower() - return parsed_host.scheme + "://" + parsed_host.netloc + ( - ':{}'.format(replace_port) if replace_port else '') + parsed_host.path + return ( + parsed_host.scheme + + "://" + + parsed_host.netloc + + (":{}".format(replace_port) if replace_port else "") + + parsed_host.path + ) def verify_url(parse_input): # noinspection PyBroadException try: - if not parse_input.startswith('http://') and not parse_input.startswith('https://'): + if not parse_input.startswith("http://") and not parse_input.startswith("https://"): # if we have a specific port, use http prefix, otherwise assume https - if ':' in parse_input: - parse_input = 'http://' + parse_input + if ":" in parse_input: + parse_input = "http://" + parse_input else: - parse_input = 'https://' + parse_input + parse_input = "https://" + parse_input parsed_host = urlparse(parse_input) - if parsed_host.scheme not in ('http', 'https'): + if parsed_host.scheme not in ("http", "https"): parsed_host = None except Exception: parsed_host = None - print('Could not parse url {}\nEnter your clearml-server host: '.format(parse_input), end='') + print("Could not parse url {}\nEnter your clearml-server host: ".format(parse_input), end="") return parsed_host -if __name__ == '__main__': +if __name__ == "__main__": main() From 01d337f1aa1dfe73d0b0271b7c156d83856b316a Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Mon, 5 Aug 2024 15:46:11 +0300 Subject: [PATCH 13/14] Black formatting --- examples/datasets/csv_dataset_creation.py | 5 +-- examples/datasets/data_ingestion.py | 20 +++-------- examples/datasets/dataset_creation.py | 8 ++--- examples/datasets/dataset_folder_syncing.py | 10 +++--- .../datasets/multi_parent_child_dataset.py | 17 ++++----- .../datasets/single_parent_child_dataset.py | 13 +++---- .../urbansounds_dataset_preprocessing.py | 36 +++++-------------- examples/datasets/urbansounds_get_data.py | 8 ++--- 8 files changed, 42 insertions(+), 75 deletions(-) diff --git a/examples/datasets/csv_dataset_creation.py b/examples/datasets/csv_dataset_creation.py index 8dbb4999..5aec21a2 100644 --- a/examples/datasets/csv_dataset_creation.py +++ b/examples/datasets/csv_dataset_creation.py @@ -6,7 +6,8 @@ def main(): print("STEP1 : Downloading CSV dataset") csv_file_path = manager.get_local_copy( - remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/Iris_Species.csv") + remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/Iris_Species.csv" + ) print("STEP2 : Creating a dataset") # By default, clearml data uploads to the clearml fileserver. Adding output_uri argument to the create() method @@ -23,5 +24,5 @@ def main(): print("We are done, have a great day :)") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/datasets/data_ingestion.py b/examples/datasets/data_ingestion.py index 638d01b2..6157da87 100644 --- a/examples/datasets/data_ingestion.py +++ b/examples/datasets/data_ingestion.py @@ -42,19 +42,13 @@ dataset_path = Dataset.get( # Dataset and Dataloader initializations transform = transforms.Compose([transforms.ToTensor()]) -trainset = datasets.CIFAR10( - root=dataset_path, train=True, download=False, transform=transform -) +trainset = datasets.CIFAR10(root=dataset_path, train=True, download=False, transform=transform) trainloader = torch.utils.data.DataLoader( trainset, batch_size=params.get("batch_size", 4), shuffle=True, num_workers=10 ) -testset = datasets.CIFAR10( - root=dataset_path, train=False, download=False, transform=transform -) -testloader = torch.utils.data.DataLoader( - testset, batch_size=params.get("batch_size", 4), shuffle=False, num_workers=10 -) +testset = datasets.CIFAR10(root=dataset_path, train=False, download=False, transform=transform) +testloader = torch.utils.data.DataLoader(testset, batch_size=params.get("batch_size", 4), shuffle=False, num_workers=10) classes = ( "plane", @@ -87,14 +81,10 @@ def predictions_gt_images_handler(engine, logger, *args, **kwargs): ax = fig.add_subplot(num_x, num_y, idx + 1, xticks=[], yticks=[]) ax.imshow(trans(x[idx])) ax.set_title( - "{0} {1:.1f}% (label: {2})".format( - classes[preds], probs * 100, classes[y[idx]] - ), + "{0} {1:.1f}% (label: {2})".format(classes[preds], probs * 100, classes[y[idx]]), color=("green" if preds == y[idx] else "red"), ) - logger.writer.add_figure( - "predictions vs actuals", figure=fig, global_step=engine.state.epoch - ) + logger.writer.add_figure("predictions vs actuals", figure=fig, global_step=engine.state.epoch) class Net(nn.Module): diff --git a/examples/datasets/dataset_creation.py b/examples/datasets/dataset_creation.py index 9fa9cc92..6a4395ce 100644 --- a/examples/datasets/dataset_creation.py +++ b/examples/datasets/dataset_creation.py @@ -3,13 +3,9 @@ from clearml import StorageManager, Dataset manager = StorageManager() -dataset_path = manager.get_local_copy( - remote_url="https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz" -) +dataset_path = manager.get_local_copy(remote_url="https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz") -dataset = Dataset.create( - dataset_name="cifar_dataset", dataset_project="dataset_examples" -) +dataset = Dataset.create(dataset_name="cifar_dataset", dataset_project="dataset_examples") # Prepare and clean data here before it is added to the dataset diff --git a/examples/datasets/dataset_folder_syncing.py b/examples/datasets/dataset_folder_syncing.py index a039642a..cefb9de8 100644 --- a/examples/datasets/dataset_folder_syncing.py +++ b/examples/datasets/dataset_folder_syncing.py @@ -8,8 +8,9 @@ from clearml import Dataset, StorageManager def download_mnist_dataset(): manager = StorageManager() - mnist_dataset = Path(manager.get_local_copy( - remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/MNIST.zip", name="MNIST")) + mnist_dataset = Path( + manager.get_local_copy(remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/MNIST.zip", name="MNIST") + ) mnist_dataset_train = mnist_dataset / "TRAIN" mnist_dataset_test = mnist_dataset / "TEST" @@ -28,7 +29,8 @@ def main(): print("STEP3 : Creating the dataset") mnist_dataset = Dataset.create( - dataset_project="dataset_examples", dataset_name="MNIST Complete Dataset (Syncing Example)") + dataset_project="dataset_examples", dataset_name="MNIST Complete Dataset (Syncing Example)" + ) print("STEP4 : Syncing train dataset") shutil.copytree(mnist_dataset_train, mnist_train_path) # Populating dataset folder with TRAIN images @@ -46,5 +48,5 @@ def main(): print("We are done, have a great day :)") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/datasets/multi_parent_child_dataset.py b/examples/datasets/multi_parent_child_dataset.py index 51f83207..a9d2dcf5 100644 --- a/examples/datasets/multi_parent_child_dataset.py +++ b/examples/datasets/multi_parent_child_dataset.py @@ -7,29 +7,30 @@ def main(): manager = StorageManager() print("STEP1 : Downloading mnist dataset") - mnist_dataset = Path(manager.get_local_copy( - remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/MNIST.zip", name="MNIST")) + mnist_dataset = Path( + manager.get_local_copy(remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/MNIST.zip", name="MNIST") + ) mnist_dataset_train = mnist_dataset / "TRAIN" mnist_dataset_test = mnist_dataset / "TEST" print("STEP2 : Creating the training dataset") - train_dataset = Dataset.create( - dataset_project="dataset_examples/MNIST", dataset_name="MNIST Training Dataset") + train_dataset = Dataset.create(dataset_project="dataset_examples/MNIST", dataset_name="MNIST Training Dataset") train_dataset.add_files(path=mnist_dataset_train, dataset_path="TRAIN") train_dataset.upload() train_dataset.finalize() print("STEP3 : Creating the testing dataset") - test_dataset = Dataset.create( - dataset_project="dataset_examples/MNIST", dataset_name="MNIST Testing Dataset") + test_dataset = Dataset.create(dataset_project="dataset_examples/MNIST", dataset_name="MNIST Testing Dataset") test_dataset.add_files(path=mnist_dataset_test, dataset_path="TEST") test_dataset.upload() test_dataset.finalize() print("STEP4 : Create a child dataset with both mnist train and test data") child_dataset = Dataset.create( - dataset_project="dataset_examples/MNIST", dataset_name="MNIST Complete Dataset", - parent_datasets=[train_dataset.id, test_dataset.id]) + dataset_project="dataset_examples/MNIST", + dataset_name="MNIST Complete Dataset", + parent_datasets=[train_dataset.id, test_dataset.id], + ) child_dataset.upload() child_dataset.finalize() diff --git a/examples/datasets/single_parent_child_dataset.py b/examples/datasets/single_parent_child_dataset.py index 664cd91c..88700f66 100644 --- a/examples/datasets/single_parent_child_dataset.py +++ b/examples/datasets/single_parent_child_dataset.py @@ -7,21 +7,22 @@ def main(): manager = StorageManager() print("STEP1 : Downloading mnist dataset") - mnist_dataset = Path(manager.get_local_copy( - remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/MNIST.zip", name="MNIST")) + mnist_dataset = Path( + manager.get_local_copy(remote_url="https://allegro-datasets.s3.amazonaws.com/datasets/MNIST.zip", name="MNIST") + ) mnist_dataset_train = mnist_dataset / "TRAIN" mnist_dataset_test = mnist_dataset / "TEST" print("STEP2 : Creating the training dataset") - mnist_dataset = Dataset.create( - dataset_project="dataset_examples", dataset_name="MNIST Training Dataset") + mnist_dataset = Dataset.create(dataset_project="dataset_examples", dataset_name="MNIST Training Dataset") mnist_dataset.add_files(path=mnist_dataset_train, dataset_path="TRAIN") mnist_dataset.upload() mnist_dataset.finalize() print("STEP3 : Create a child dataset of mnist dataset using TEST Dataset") child_dataset = Dataset.create( - dataset_project="dataset_examples", dataset_name="MNIST Complete Dataset", parent_datasets=[mnist_dataset.id]) + dataset_project="dataset_examples", dataset_name="MNIST Complete Dataset", parent_datasets=[mnist_dataset.id] + ) child_dataset.add_files(path=mnist_dataset_test, dataset_path="TEST") child_dataset.upload() child_dataset.finalize() @@ -29,5 +30,5 @@ def main(): print("We are done, have a great day :)") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/datasets/urbansounds_dataset_preprocessing.py b/examples/datasets/urbansounds_dataset_preprocessing.py index 7230afb9..19b418d5 100644 --- a/examples/datasets/urbansounds_dataset_preprocessing.py +++ b/examples/datasets/urbansounds_dataset_preprocessing.py @@ -43,9 +43,7 @@ class PreProcessor: # Make sure all spectrograms are the same size fixed_length = 3 * (self.configuration["resample_freq"] // 200) if melspectogram_db.shape[2] < fixed_length: - melspectogram_db = torch.nn.functional.pad( - melspectogram_db, (0, fixed_length - melspectogram_db.shape[2]) - ) + melspectogram_db = torch.nn.functional.pad(melspectogram_db, (0, fixed_length - melspectogram_db.shape[2])) else: melspectogram_db = melspectogram_db[:, :, :fixed_length] @@ -64,16 +62,10 @@ class DataSetBuilder: alias="Raw Dataset", ) # This will return the pandas dataframe we added in the previous task - self.metadata = ( - Task.get_task(task_id=self.original_dataset._task.id) - .artifacts["metadata"] - .get() - ) + self.metadata = Task.get_task(task_id=self.original_dataset._task.id).artifacts["metadata"].get() # This will download the data and return a local path to the data self.original_dataset_path = Path( - self.original_dataset.get_mutable_local_copy( - self.configuration["dataset_path"], overwrite=True - ) + self.original_dataset.get_mutable_local_copy(self.configuration["dataset_path"], overwrite=True) ) # Prepare a preprocessor that will handle each sample one by one @@ -114,33 +106,23 @@ class DataSetBuilder: # audio side by side in the debug sample UI) for i, (_, data) in tqdm(enumerate(self.metadata.iterrows())): _, audio_file_path, label = data.tolist() - sample, sample_freq = torchaudio.load( - self.original_dataset_path / audio_file_path, normalize=True - ) + sample, sample_freq = torchaudio.load(self.original_dataset_path / audio_file_path, normalize=True) spectrogram = self.preprocessor.preprocess_sample(sample, sample_freq) # Get only the filename and replace the extension, we're saving an image here new_file_name = os.path.basename(audio_file_path).replace(".wav", ".npy") # Get the correct folder, basically the original dataset folder + the new filename - spectrogram_path = ( - self.original_dataset_path - / os.path.dirname(audio_file_path) - / new_file_name - ) + spectrogram_path = self.original_dataset_path / os.path.dirname(audio_file_path) / new_file_name # Save the numpy array to disk np.save(spectrogram_path, spectrogram) # Log every 10th sample as a debug sample to the UI, so we can manually check it if i % 10 == 0: # Convert the numpy array to a viewable JPEG - rgb_image = mpl.colormaps["viridis"]( - spectrogram[0, :, :].detach().numpy() * 255 - )[:, :, :3] + rgb_image = mpl.colormaps["viridis"](spectrogram[0, :, :].detach().numpy() * 255)[:, :, :3] title = os.path.splitext(os.path.basename(audio_file_path))[0] # Report the image and the original sound, so they can be viewed side by side - self.preprocessed_dataset.get_logger().report_image( - title=title, series="spectrogram", image=rgb_image - ) + self.preprocessed_dataset.get_logger().report_image(title=title, series="spectrogram", image=rgb_image) self.preprocessed_dataset.get_logger().report_media( title=title, series="original_audio", @@ -152,9 +134,7 @@ class DataSetBuilder: # Again add some visualizations to the task self.log_dataset_statistics() # We still want the metadata - self.preprocessed_dataset._task.upload_artifact( - name="metadata", artifact_object=self.metadata - ) + self.preprocessed_dataset._task.upload_artifact(name="metadata", artifact_object=self.metadata) self.preprocessed_dataset.finalize(auto_upload=True) diff --git a/examples/datasets/urbansounds_get_data.py b/examples/datasets/urbansounds_get_data.py index 9f442077..55e6f352 100644 --- a/examples/datasets/urbansounds_get_data.py +++ b/examples/datasets/urbansounds_get_data.py @@ -28,9 +28,7 @@ def get_urbansound8k(): "https://allegro-datasets.s3.amazonaws.com/clearml/UrbanSound8K.zip", extract_archive=True, ) - path_to_urbansound8k_csv = ( - Path(path_to_urbansound8k) / "UrbanSound8K" / "metadata" / "UrbanSound8K.csv" - ) + path_to_urbansound8k_csv = Path(path_to_urbansound8k) / "UrbanSound8K" / "metadata" / "UrbanSound8K.csv" path_to_urbansound8k_audio = Path(path_to_urbansound8k) / "UrbanSound8K" / "audio" return path_to_urbansound8k_csv, path_to_urbansound8k_audio @@ -38,9 +36,7 @@ def get_urbansound8k(): def log_dataset_statistics(dataset, metadata): histogram_data = metadata["class"].value_counts() - dataset.get_logger().report_table( - title="Raw Dataset Metadata", series="Raw Dataset Metadata", table_plot=metadata - ) + dataset.get_logger().report_table(title="Raw Dataset Metadata", series="Raw Dataset Metadata", table_plot=metadata) dataset.get_logger().report_histogram( title="Class distribution", series="Class distribution", From 97221b6c152e72c34940ed68fc97b9258677ea1f Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Tue, 6 Aug 2024 16:26:06 +0300 Subject: [PATCH 14/14] Version bump to v1.16.3 --- clearml/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clearml/version.py b/clearml/version.py index 9e54f289..6cc7415d 100644 --- a/clearml/version.py +++ b/clearml/version.py @@ -1 +1 @@ -__version__ = "1.16.2" +__version__ = "1.16.3"