idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- idmtools/__init__.py +27 -8
- idmtools/analysis/__init__.py +5 -0
- idmtools/analysis/add_analyzer.py +89 -0
- idmtools/analysis/analyze_manager.py +490 -0
- idmtools/analysis/csv_analyzer.py +103 -0
- idmtools/analysis/download_analyzer.py +96 -0
- idmtools/analysis/map_worker_entry.py +100 -0
- idmtools/analysis/platform_analysis_bootstrap.py +94 -0
- idmtools/analysis/platform_anaylsis.py +291 -0
- idmtools/analysis/tags_analyzer.py +93 -0
- idmtools/assets/__init__.py +9 -0
- idmtools/assets/asset.py +453 -0
- idmtools/assets/asset_collection.py +514 -0
- idmtools/assets/content_handlers.py +19 -0
- idmtools/assets/errors.py +23 -0
- idmtools/assets/file_list.py +191 -0
- idmtools/builders/__init__.py +11 -0
- idmtools/builders/arm_simulation_builder.py +152 -0
- idmtools/builders/csv_simulation_builder.py +76 -0
- idmtools/builders/simulation_builder.py +348 -0
- idmtools/builders/sweep_arm.py +109 -0
- idmtools/builders/yaml_simulation_builder.py +82 -0
- idmtools/config/__init__.py +7 -0
- idmtools/config/idm_config_parser.py +486 -0
- idmtools/core/__init__.py +10 -0
- idmtools/core/cache_enabled.py +114 -0
- idmtools/core/context.py +68 -0
- idmtools/core/docker_task.py +207 -0
- idmtools/core/enums.py +51 -0
- idmtools/core/exceptions.py +91 -0
- idmtools/core/experiment_factory.py +71 -0
- idmtools/core/id_file.py +70 -0
- idmtools/core/interfaces/__init__.py +5 -0
- idmtools/core/interfaces/entity_container.py +64 -0
- idmtools/core/interfaces/iassets_enabled.py +58 -0
- idmtools/core/interfaces/ientity.py +331 -0
- idmtools/core/interfaces/iitem.py +206 -0
- idmtools/core/interfaces/imetadata_operations.py +89 -0
- idmtools/core/interfaces/inamed_entity.py +17 -0
- idmtools/core/interfaces/irunnable_entity.py +159 -0
- idmtools/core/logging.py +387 -0
- idmtools/core/platform_factory.py +316 -0
- idmtools/core/system_information.py +104 -0
- idmtools/core/task_factory.py +145 -0
- idmtools/entities/__init__.py +10 -0
- idmtools/entities/command_line.py +229 -0
- idmtools/entities/command_task.py +155 -0
- idmtools/entities/experiment.py +787 -0
- idmtools/entities/generic_workitem.py +43 -0
- idmtools/entities/ianalyzer.py +163 -0
- idmtools/entities/iplatform.py +1106 -0
- idmtools/entities/iplatform_default.py +39 -0
- idmtools/entities/iplatform_ops/__init__.py +5 -0
- idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
- idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
- idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
- idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
- idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
- idmtools/entities/iplatform_ops/utils.py +185 -0
- idmtools/entities/itask.py +316 -0
- idmtools/entities/iworkflow_item.py +167 -0
- idmtools/entities/platform_requirements.py +20 -0
- idmtools/entities/relation_type.py +14 -0
- idmtools/entities/simulation.py +255 -0
- idmtools/entities/suite.py +188 -0
- idmtools/entities/task_proxy.py +37 -0
- idmtools/entities/templated_simulation.py +325 -0
- idmtools/frozen/frozen_dict.py +71 -0
- idmtools/frozen/frozen_list.py +66 -0
- idmtools/frozen/frozen_set.py +86 -0
- idmtools/frozen/frozen_tuple.py +18 -0
- idmtools/frozen/frozen_utils.py +179 -0
- idmtools/frozen/ifrozen.py +66 -0
- idmtools/plugins/__init__.py +5 -0
- idmtools/plugins/git_commit.py +117 -0
- idmtools/registry/__init__.py +4 -0
- idmtools/registry/experiment_specification.py +105 -0
- idmtools/registry/functions.py +28 -0
- idmtools/registry/hook_specs.py +132 -0
- idmtools/registry/master_plugin_registry.py +51 -0
- idmtools/registry/platform_specification.py +138 -0
- idmtools/registry/plugin_specification.py +129 -0
- idmtools/registry/task_specification.py +104 -0
- idmtools/registry/utils.py +119 -0
- idmtools/services/__init__.py +5 -0
- idmtools/services/ipersistance_service.py +135 -0
- idmtools/services/platforms.py +13 -0
- idmtools/utils/__init__.py +5 -0
- idmtools/utils/caller.py +24 -0
- idmtools/utils/collections.py +246 -0
- idmtools/utils/command_line.py +45 -0
- idmtools/utils/decorators.py +300 -0
- idmtools/utils/display/__init__.py +22 -0
- idmtools/utils/display/displays.py +181 -0
- idmtools/utils/display/settings.py +25 -0
- idmtools/utils/dropbox_location.py +30 -0
- idmtools/utils/entities.py +127 -0
- idmtools/utils/file.py +72 -0
- idmtools/utils/file_parser.py +151 -0
- idmtools/utils/filter_simulations.py +182 -0
- idmtools/utils/filters/__init__.py +5 -0
- idmtools/utils/filters/asset_filters.py +88 -0
- idmtools/utils/general.py +286 -0
- idmtools/utils/gitrepo.py +336 -0
- idmtools/utils/hashing.py +239 -0
- idmtools/utils/info.py +124 -0
- idmtools/utils/json.py +82 -0
- idmtools/utils/language.py +107 -0
- idmtools/utils/local_os.py +40 -0
- idmtools/utils/time.py +22 -0
- idmtools-0.0.2.dist-info/METADATA +120 -0
- idmtools-0.0.2.dist-info/RECORD +116 -0
- idmtools-0.0.2.dist-info/entry_points.txt +9 -0
- idmtools-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
- idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
- idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/WHEEL +0 -0
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IRunnableEntity definition. IRunnableEntity defines items that can be ran using platform.run().
|
|
3
|
+
|
|
4
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import field, dataclass
|
|
7
|
+
from inspect import signature
|
|
8
|
+
from logging import getLogger, DEBUG
|
|
9
|
+
from typing import List, Callable, TYPE_CHECKING, NoReturn
|
|
10
|
+
from abc import ABCMeta
|
|
11
|
+
from idmtools.core import EntityStatus
|
|
12
|
+
from idmtools.core.interfaces.ientity import IEntity
|
|
13
|
+
from idmtools.registry.functions import FunctionPluginManager
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
from idmtools.entities.iplatform import IPlatform
|
|
17
|
+
|
|
18
|
+
runnable_hook = Callable[['IRunnableEntity', 'IPlatform'], None]
|
|
19
|
+
logger = getLogger(__name__)
|
|
20
|
+
user_logger = getLogger('user')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class IRunnableEntity(IEntity, metaclass=ABCMeta):
|
|
25
|
+
"""
|
|
26
|
+
IRunnableEntity are items that can be ran on platforms like Experiments or WorkItems.
|
|
27
|
+
|
|
28
|
+
IRunnableEntity also add pre and post run hooks available to the IEntity class.
|
|
29
|
+
"""
|
|
30
|
+
__pre_run_hooks: List[runnable_hook] = field(default_factory=list, metadata={"md": True})
|
|
31
|
+
__post_run_hooks: List[runnable_hook] = field(default_factory=list, metadata={"md": True})
|
|
32
|
+
|
|
33
|
+
def pre_run(self, platform: 'IPlatform') -> None:
|
|
34
|
+
"""
|
|
35
|
+
Called before the actual creation of the entity.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
platform: Platform item is being created on
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
None
|
|
42
|
+
"""
|
|
43
|
+
for hook in self.__pre_run_hooks:
|
|
44
|
+
if logger.isEnabledFor(DEBUG):
|
|
45
|
+
logger.debug(f'Calling pre-create hook named {hook.__name__ if hasattr(hook, "__name__") else str(hook)}')
|
|
46
|
+
hook(self, platform)
|
|
47
|
+
|
|
48
|
+
def post_run(self, platform: 'IPlatform') -> None:
|
|
49
|
+
"""
|
|
50
|
+
Called after the actual creation of the entity.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
platform: Platform item was created on
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
None
|
|
57
|
+
"""
|
|
58
|
+
for hook in self.__post_run_hooks:
|
|
59
|
+
if logger.isEnabledFor(DEBUG):
|
|
60
|
+
logger.debug(f'Calling pre-create hook named {hook.__name__ if hasattr(hook, "__name__") else str(hook)}')
|
|
61
|
+
hook(self, platform)
|
|
62
|
+
|
|
63
|
+
def add_pre_run_hook(self, hook: runnable_hook):
|
|
64
|
+
"""
|
|
65
|
+
Adds a hook function to be called before an item is ran.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
hook: Hook function. This should have two arguments, the item and the platform
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
None
|
|
72
|
+
"""
|
|
73
|
+
if len(signature(hook).parameters) != 2:
|
|
74
|
+
raise ValueError("Pre Run hooks should have 2 arguments. The first argument will be the item, the second the platform")
|
|
75
|
+
self.__pre_run_hooks.append(hook)
|
|
76
|
+
|
|
77
|
+
def add_post_run_hook(self, hook: runnable_hook):
|
|
78
|
+
"""
|
|
79
|
+
Adds a hook function to be called after an item has ran.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
hook: Hook function. This should have two arguments, the item and the platform
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
None
|
|
86
|
+
"""
|
|
87
|
+
if len(signature(hook).parameters) != 2:
|
|
88
|
+
raise ValueError("Post Run hooks should have 2 arguments. The first argument will be the item, the second the platform")
|
|
89
|
+
self.__post_run_hooks.append(hook)
|
|
90
|
+
|
|
91
|
+
def run(self, wait_until_done: bool = False, platform: 'IPlatform' = None, wait_on_done_progress: bool = True, **run_opts) -> NoReturn:
|
|
92
|
+
"""
|
|
93
|
+
Runs an item.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
wait_until_done: Whether we should wait on item to finish running as well. Defaults to False
|
|
97
|
+
platform: Platform object to use. If not specified, we first check object for platform object then the current context
|
|
98
|
+
wait_on_done_progress: Defaults to true
|
|
99
|
+
**run_opts: Options to pass to the platform
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
None
|
|
103
|
+
"""
|
|
104
|
+
p = super()._check_for_platform_from_context(platform)
|
|
105
|
+
if 'wait_on_done' in run_opts:
|
|
106
|
+
raise TypeError("The 'wait_on_done' parameter has been removed in idmtools 1.8.0. Please update your code with 'wait_until_done'.")
|
|
107
|
+
p.run_items(self, wait_on_done_progress=wait_on_done_progress, **run_opts)
|
|
108
|
+
dry_run = run_opts.get('dry_run', False)
|
|
109
|
+
if dry_run:
|
|
110
|
+
return
|
|
111
|
+
if wait_until_done:
|
|
112
|
+
_refresh_interval = run_opts.get('refresh_interval', None)
|
|
113
|
+
if _refresh_interval is None:
|
|
114
|
+
_refresh_interval = p.refresh_interval
|
|
115
|
+
self.wait(wait_on_done_progress=wait_on_done_progress, platform=p, refresh_interval=_refresh_interval)
|
|
116
|
+
|
|
117
|
+
def wait(self, wait_on_done_progress: bool = True, timeout: int = None, refresh_interval=None, platform: 'IPlatform' = None, **kwargs):
|
|
118
|
+
"""
|
|
119
|
+
Wait on an item to finish running.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
wait_on_done_progress: Should we show progress as we wait?
|
|
123
|
+
timeout: Timeout to wait
|
|
124
|
+
refresh_interval: How often to refresh object
|
|
125
|
+
platform: Platform. If not specified, we try to determine this from context
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
None
|
|
129
|
+
"""
|
|
130
|
+
# If done, exit
|
|
131
|
+
if self.status in [EntityStatus.SUCCEEDED, EntityStatus.FAILED]:
|
|
132
|
+
return
|
|
133
|
+
if self.status not in [EntityStatus.CREATED, EntityStatus.RUNNING]:
|
|
134
|
+
raise ValueError("The item cannot be waited for if it is not in Running/Created state")
|
|
135
|
+
opts = dict(**kwargs)
|
|
136
|
+
if timeout:
|
|
137
|
+
opts['timeout'] = timeout
|
|
138
|
+
if refresh_interval:
|
|
139
|
+
opts['refresh_interval'] = refresh_interval
|
|
140
|
+
p = super()._check_for_platform_from_context(platform)
|
|
141
|
+
if wait_on_done_progress:
|
|
142
|
+
p.wait_till_done_progress(self, **opts)
|
|
143
|
+
else:
|
|
144
|
+
p.wait_till_done(self, **opts)
|
|
145
|
+
|
|
146
|
+
self.after_done()
|
|
147
|
+
|
|
148
|
+
def after_done(self):
|
|
149
|
+
"""
|
|
150
|
+
Run after an item is done after waiting. Currently we call the on succeeded and on failure plugins.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Runs after an item is done after waiting
|
|
154
|
+
"""
|
|
155
|
+
FunctionPluginManager.instance().hook.idmtools_runnable_on_done(item=self)
|
|
156
|
+
if self.succeeded:
|
|
157
|
+
FunctionPluginManager.instance().hook.idmtools_runnable_on_succeeded(item=self)
|
|
158
|
+
else:
|
|
159
|
+
FunctionPluginManager.instance().hook.idmtools_runnable_on_failure(item=self)
|
idmtools/core/logging.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
idmtools logging module.
|
|
3
|
+
|
|
4
|
+
We configure our logging here, manage multi-process logging, alternate logging level, and additional utilities to manage logging.
|
|
5
|
+
|
|
6
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
from contextlib import suppress
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from logging import getLogger
|
|
16
|
+
from logging.handlers import RotatingFileHandler
|
|
17
|
+
from typing import Union, Optional
|
|
18
|
+
import coloredlogs as coloredlogs
|
|
19
|
+
from idmtools.core import TRUTHY_VALUES
|
|
20
|
+
|
|
21
|
+
LOGGING_STARTED = False
|
|
22
|
+
LOGGING_FILE_STARTED = False
|
|
23
|
+
LOGGING_FILE_HANDLER = None
|
|
24
|
+
|
|
25
|
+
VERBOSE = 15
|
|
26
|
+
NOTICE = 25
|
|
27
|
+
SUCCESS = 35
|
|
28
|
+
CRITICAL = 50
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class IdmToolsLoggingConfig:
|
|
33
|
+
"""
|
|
34
|
+
Defines the config options available for idmtools logs.
|
|
35
|
+
"""
|
|
36
|
+
#: Console level
|
|
37
|
+
level: Union[str, int] = logging.WARN
|
|
38
|
+
#: Filename for idmtools logs
|
|
39
|
+
filename: Optional[str] = 'idmtools.log'
|
|
40
|
+
#: Toggle to enable/disable console logging
|
|
41
|
+
console: bool = False
|
|
42
|
+
#: File log level
|
|
43
|
+
file_level: Union[str, int] = 'DEBUG'
|
|
44
|
+
#: Should we force reload
|
|
45
|
+
force: bool = False
|
|
46
|
+
#: File format string. See https://docs.python.org/3/library/logging.html#logrecord-attributes for format vars
|
|
47
|
+
file_log_format_str: str = None
|
|
48
|
+
#: Logging format. See https://docs.python.org/3/library/logging.html#logrecord-attributes for format vars
|
|
49
|
+
user_log_format_str: str = '%(message)s'
|
|
50
|
+
#: Toggle to enable/disable coloredlogs
|
|
51
|
+
use_colored_logs: bool = True
|
|
52
|
+
#: Toggle user output. This should only be used in certain situations like CLI's that output JSON
|
|
53
|
+
user_output: bool = True
|
|
54
|
+
#: Toggle enable file logging
|
|
55
|
+
enable_file_logging: Union[str, bool] = True
|
|
56
|
+
|
|
57
|
+
def __post_init__(self):
|
|
58
|
+
"""
|
|
59
|
+
Validates logging config creation.
|
|
60
|
+
"""
|
|
61
|
+
# load the user input from string
|
|
62
|
+
if isinstance(self.user_output, str):
|
|
63
|
+
self.user_output = self.user_output.lower() in TRUTHY_VALUES
|
|
64
|
+
|
|
65
|
+
# load color logs from string
|
|
66
|
+
if isinstance(self.use_colored_logs, str):
|
|
67
|
+
self.use_colored_logs = self.use_colored_logs.lower() in TRUTHY_VALUES
|
|
68
|
+
|
|
69
|
+
# load console from string
|
|
70
|
+
if isinstance(self.console, str):
|
|
71
|
+
self.console = self.console.lower() in TRUTHY_VALUES
|
|
72
|
+
|
|
73
|
+
if type(self.enable_file_logging) is str:
|
|
74
|
+
self.enable_file_logging = self.enable_file_logging.lower() in TRUTHY_VALUES
|
|
75
|
+
|
|
76
|
+
# ensure level is a logging level
|
|
77
|
+
for attr in ['level', 'file_level']:
|
|
78
|
+
if isinstance(getattr(self, attr), str):
|
|
79
|
+
setattr(self, attr, logging.getLevelName(getattr(self, attr)))
|
|
80
|
+
|
|
81
|
+
# set default name is not set.
|
|
82
|
+
if self.filename is None:
|
|
83
|
+
self.filename = 'idmtools.log'
|
|
84
|
+
|
|
85
|
+
# ensure whitespace is gone
|
|
86
|
+
self.filename = self.filename.strip()
|
|
87
|
+
|
|
88
|
+
# disable file logging when set to -1
|
|
89
|
+
if self.filename == "-1" or not self.enable_file_logging:
|
|
90
|
+
self.filename = None
|
|
91
|
+
|
|
92
|
+
# handle special case
|
|
93
|
+
if not self.enable_file_logging and self.console is None:
|
|
94
|
+
self.console = True
|
|
95
|
+
|
|
96
|
+
# set default file log format str
|
|
97
|
+
if self.file_log_format_str is None:
|
|
98
|
+
if self.file_level == logging.DEBUG:
|
|
99
|
+
# Enable detailed logging format
|
|
100
|
+
self.file_log_format_str = '%(asctime)s.%(msecs)d %(pathname)s:%(lineno)d %(funcName)s [%(levelname)s] (%(process)d,%(thread)d) - %(message)s'
|
|
101
|
+
else:
|
|
102
|
+
self.file_log_format_str = '%(asctime)s.%(msecs)d %(pathname)s:%(lineno)d %(funcName)s [%(levelname)s] - %(message)s'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MultiProcessSafeRotatingFileHandler(RotatingFileHandler):
|
|
106
|
+
"""
|
|
107
|
+
Multi-process safe logger.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False):
|
|
111
|
+
"""
|
|
112
|
+
See RotatingFileHandler for full details on arguments.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
filename:Filename to use
|
|
116
|
+
mode:Mode
|
|
117
|
+
maxBytes: Max bytes
|
|
118
|
+
backupCount: Total backups
|
|
119
|
+
encoding: Encoding
|
|
120
|
+
delay: Delay
|
|
121
|
+
"""
|
|
122
|
+
super().__init__(filename, mode=mode, maxBytes=maxBytes, backupCount=backupCount, encoding=encoding,
|
|
123
|
+
delay=delay)
|
|
124
|
+
self.logger_lock = threading.Lock()
|
|
125
|
+
|
|
126
|
+
def handle(self, record: logging.LogRecord) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Thread safe logger.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
record: Record to handle
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
None
|
|
135
|
+
"""
|
|
136
|
+
self.logger_lock.acquire()
|
|
137
|
+
try:
|
|
138
|
+
super(MultiProcessSafeRotatingFileHandler, self).handle(record)
|
|
139
|
+
finally:
|
|
140
|
+
self.logger_lock.release()
|
|
141
|
+
|
|
142
|
+
def doRollover(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Perform rollover safely.
|
|
145
|
+
|
|
146
|
+
We loop and try to move the log file. If we encounter an issue, we try to retry three times.
|
|
147
|
+
If we failed after three times, we try a new process id appended to file name.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
None
|
|
151
|
+
"""
|
|
152
|
+
attempts = 0
|
|
153
|
+
while True:
|
|
154
|
+
try:
|
|
155
|
+
super().doRollover()
|
|
156
|
+
break
|
|
157
|
+
except PermissionError:
|
|
158
|
+
attempts += 1
|
|
159
|
+
if attempts > 3:
|
|
160
|
+
# add a pid to make unique or expand
|
|
161
|
+
self.baseFilename += f".{os.getpid()}"
|
|
162
|
+
attempts = 0
|
|
163
|
+
time.sleep(0.08)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class PrintHandler(logging.Handler):
|
|
167
|
+
"""
|
|
168
|
+
A simple print handler. Used in cases where logging fails.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def handle(self, record: logging.LogRecord) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Simple log handler that prints to stdout.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
record: Record to print
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
None
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
print(record.msg)
|
|
183
|
+
except: # noqa: E722
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def setup_logging(logging_config: IdmToolsLoggingConfig) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Set up logging.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
logging_config: IdmToolsLoggingConfig that defines our config
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Returns None
|
|
196
|
+
|
|
197
|
+
See Also:
|
|
198
|
+
For logging levels, see https://coloredlogs.readthedocs.io/en/latest/api.html#id26
|
|
199
|
+
"""
|
|
200
|
+
global LOGGING_STARTED, LOGGING_FILE_STARTED, LOGGING_FILE_HANDLER
|
|
201
|
+
if not LOGGING_STARTED or logging_config.force:
|
|
202
|
+
# reset logging and remove all handlers and delete our current handler
|
|
203
|
+
reset_logging_handlers()
|
|
204
|
+
|
|
205
|
+
logging.addLevelName(15, 'VERBOSE')
|
|
206
|
+
logging.addLevelName(25, 'NOTICE')
|
|
207
|
+
logging.addLevelName(35, 'SUCCESS')
|
|
208
|
+
|
|
209
|
+
# get a file handler
|
|
210
|
+
root = logging.getLogger()
|
|
211
|
+
user = logging.getLogger('user')
|
|
212
|
+
# allow setting the debug of logger via environment variable
|
|
213
|
+
root.setLevel(logging_config.level)
|
|
214
|
+
user.setLevel(logging.DEBUG)
|
|
215
|
+
|
|
216
|
+
# file logging config
|
|
217
|
+
if not LOGGING_FILE_STARTED or logging_config.force:
|
|
218
|
+
LOGGING_FILE_HANDLER = setup_handlers(logging_config)
|
|
219
|
+
if LOGGING_FILE_HANDLER:
|
|
220
|
+
LOGGING_FILE_STARTED = True
|
|
221
|
+
|
|
222
|
+
# Show we enable user output. The only time we really should not do this is for specific CLI use cases
|
|
223
|
+
# # such as json output
|
|
224
|
+
setup_user_logger(logging_config)
|
|
225
|
+
|
|
226
|
+
if root.isEnabledFor(logging.DEBUG):
|
|
227
|
+
from idmtools import __version__
|
|
228
|
+
root.debug(f"idmtools core version: {__version__}")
|
|
229
|
+
|
|
230
|
+
# python logger creates default stream handler when no handlers are set so create null handler
|
|
231
|
+
if len(root.handlers) == 0:
|
|
232
|
+
root.addHandler(logging.NullHandler())
|
|
233
|
+
|
|
234
|
+
# set file logging stated
|
|
235
|
+
LOGGING_FILE_STARTED = True
|
|
236
|
+
# set logging stated
|
|
237
|
+
LOGGING_STARTED = True
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def setup_handlers(logging_config: IdmToolsLoggingConfig):
|
|
241
|
+
"""
|
|
242
|
+
Setup Handlers for Global and user Loggers.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
logging_config: Logging config
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
FileHandler or None
|
|
249
|
+
"""
|
|
250
|
+
# We only one to do this setup once per process. Having the logging_queue setup help prevent that issue
|
|
251
|
+
# get a file handler
|
|
252
|
+
|
|
253
|
+
exclude_logging_classes()
|
|
254
|
+
file_handler = None
|
|
255
|
+
if logging_config.filename:
|
|
256
|
+
formatter = logging.Formatter(logging_config.file_log_format_str)
|
|
257
|
+
# set the logging to either common level or the file-level
|
|
258
|
+
file_handler = set_file_logging(logging_config, formatter)
|
|
259
|
+
|
|
260
|
+
# If user output is enabled and console is enabled
|
|
261
|
+
if logging_config.user_output and logging_config.console:
|
|
262
|
+
# is colored logging enabled? if so, add our logger
|
|
263
|
+
# note: this will be used for user logging as well
|
|
264
|
+
if logging_config.use_colored_logs:
|
|
265
|
+
coloredlogs.install(level=logging_config.level, milliseconds=True, stream=sys.stdout)
|
|
266
|
+
else:
|
|
267
|
+
# Mainly for test/local platform
|
|
268
|
+
print_handler = PrintHandler(level=logging_config.level)
|
|
269
|
+
getLogger().addHandler(print_handler)
|
|
270
|
+
return file_handler
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def setup_user_logger(logging_config: IdmToolsLoggingConfig):
|
|
274
|
+
"""
|
|
275
|
+
Setup the user logger. This logger is meant for user output only.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
logging_config: Logging config object.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
None
|
|
282
|
+
"""
|
|
283
|
+
if logging_config.user_output and not logging_config.console:
|
|
284
|
+
# is colored logs enabled? If so, make the user logger a coloredlogger
|
|
285
|
+
if logging_config.use_colored_logs:
|
|
286
|
+
coloredlogs.install(logger=getLogger('user'), level=VERBOSE, fmt='%(message)s', stream=sys.stdout)
|
|
287
|
+
else: # fall back to a print handler
|
|
288
|
+
setup_user_print_logger()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def setup_user_print_logger():
|
|
292
|
+
"""
|
|
293
|
+
Setup a print based logger for user messages.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
None
|
|
297
|
+
"""
|
|
298
|
+
formatter = logging.Formatter(fmt='%(message)s')
|
|
299
|
+
handler = PrintHandler(level=VERBOSE)
|
|
300
|
+
handler.setFormatter(formatter)
|
|
301
|
+
# should everything be printed using the print logger or filename was set to be empty. This means log
|
|
302
|
+
# everything to the screen without color
|
|
303
|
+
getLogger('user').addHandler(handler)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def set_file_logging(logging_config: IdmToolsLoggingConfig, formatter: logging.Formatter):
|
|
307
|
+
"""
|
|
308
|
+
Set File Logging.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
logging_config: Logging config object.
|
|
312
|
+
formatter: Formatter obj
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Return File handler
|
|
316
|
+
"""
|
|
317
|
+
file_handler = create_file_handler(logging_config.file_level, formatter, logging_config.filename)
|
|
318
|
+
if file_handler is None:
|
|
319
|
+
# We had an issue creating file handler, so let's try using default name + pids
|
|
320
|
+
for i in range(64): # We go to 64. This is a reasonable max id for any computer we might actually run item.
|
|
321
|
+
file_handler = create_file_handler(logging_config.file_level, formatter,
|
|
322
|
+
f"{logging_config.filename}.{i}.log")
|
|
323
|
+
if file_handler:
|
|
324
|
+
break
|
|
325
|
+
if file_handler is None:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
"Could not file a valid log. Either all the files are opened or you are on a read-only filesystem. You can disable file-based logging by setting")
|
|
328
|
+
logging.root.addHandler(file_handler)
|
|
329
|
+
return file_handler
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def create_file_handler(file_level, formatter: logging.Formatter, filename: str):
|
|
333
|
+
"""
|
|
334
|
+
Create a MultiProcessSafeRotatingFileHandler for idmtools.log.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
file_level: Level to log to file
|
|
338
|
+
formatter: Formatter to set on the handler
|
|
339
|
+
filename: Filename to use
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
SafeRotatingFileHandler with properties provided
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
file_handler = MultiProcessSafeRotatingFileHandler(filename, maxBytes=(2 ** 20) * 10, backupCount=5)
|
|
346
|
+
file_handler.setLevel(file_level)
|
|
347
|
+
file_handler.setFormatter(formatter)
|
|
348
|
+
except PermissionError:
|
|
349
|
+
return None
|
|
350
|
+
return file_handler
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def reset_logging_handlers():
|
|
354
|
+
"""
|
|
355
|
+
Reset all the logging handlers by removing the root handler.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
None
|
|
359
|
+
"""
|
|
360
|
+
global LOGGING_STARTED, LOGGING_FILE_STARTED, LOGGING_FILE_HANDLER
|
|
361
|
+
|
|
362
|
+
with suppress(KeyError):
|
|
363
|
+
# Remove all handlers associated with the root logger object.
|
|
364
|
+
for handler in logging.root.handlers[:]:
|
|
365
|
+
logging.root.removeHandler(handler)
|
|
366
|
+
# Clean up file handler now
|
|
367
|
+
LOGGING_FILE_STARTED = False
|
|
368
|
+
LOGGING_STARTED = False
|
|
369
|
+
LOGGING_FILE_HANDLER = None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def exclude_logging_classes(items_to_exclude=None):
|
|
373
|
+
"""
|
|
374
|
+
Exclude items from our logger by setting level to warning.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
items_to_exclude: Items to exclude
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
None
|
|
381
|
+
"""
|
|
382
|
+
if items_to_exclude is None:
|
|
383
|
+
items_to_exclude = ['urllib3', 'paramiko', 'matplotlib']
|
|
384
|
+
# remove comps by default
|
|
385
|
+
for item in items_to_exclude:
|
|
386
|
+
other_logger = getLogger(item)
|
|
387
|
+
other_logger.setLevel(logging.WARN)
|