automation-traverse 0.1.0__tar.gz

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.
Files changed (31) hide show
  1. automation_traverse-0.1.0/PKG-INFO +23 -0
  2. automation_traverse-0.1.0/README.md +5 -0
  3. automation_traverse-0.1.0/automation_traverse/__init__.py +1 -0
  4. automation_traverse-0.1.0/automation_traverse/asserter.py +48 -0
  5. automation_traverse-0.1.0/automation_traverse/context.py +202 -0
  6. automation_traverse-0.1.0/automation_traverse/discover.py +85 -0
  7. automation_traverse-0.1.0/automation_traverse/emitters/__init__.py +3 -0
  8. automation_traverse-0.1.0/automation_traverse/emitters/emitter.py +152 -0
  9. automation_traverse-0.1.0/automation_traverse/emitters/simple_log.py +79 -0
  10. automation_traverse-0.1.0/automation_traverse/emitters/terminal.py +67 -0
  11. automation_traverse-0.1.0/automation_traverse/emitters/test_emitter.py +35 -0
  12. automation_traverse-0.1.0/automation_traverse/emitters/test_simple_log.py +78 -0
  13. automation_traverse-0.1.0/automation_traverse/emitters/test_terminal.py +54 -0
  14. automation_traverse-0.1.0/automation_traverse/runner.py +697 -0
  15. automation_traverse-0.1.0/automation_traverse/task.py +471 -0
  16. automation_traverse-0.1.0/automation_traverse/test_context/__init__.py +0 -0
  17. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_context/__init__.py +0 -0
  18. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_context/common.py +10 -0
  19. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_context/test_add_emitter.py +12 -0
  20. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_context/test_pop_subcontext.py +17 -0
  21. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_context/test_subcontext.py +31 -0
  22. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_file.py +63 -0
  23. automation_traverse-0.1.0/automation_traverse/test_context/test_traverse_subcontext.py +38 -0
  24. automation_traverse-0.1.0/automation_traverse/test_discover/__init__.py +0 -0
  25. automation_traverse-0.1.0/automation_traverse/test_discover/test_import_module.py +62 -0
  26. automation_traverse-0.1.0/automation_traverse.egg-info/PKG-INFO +23 -0
  27. automation_traverse-0.1.0/automation_traverse.egg-info/SOURCES.txt +29 -0
  28. automation_traverse-0.1.0/automation_traverse.egg-info/dependency_links.txt +1 -0
  29. automation_traverse-0.1.0/automation_traverse.egg-info/top_level.txt +1 -0
  30. automation_traverse-0.1.0/pyproject.toml +24 -0
  31. automation_traverse-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: automation-traverse
3
+ Version: 0.1.0
4
+ Summary: Automate tasks in graph format
5
+ Author-email: Michael Renken <michaelarenken@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SudoVim/automation-traverse
8
+ Project-URL: Issues, https://github.com/SudoVim/automation-traverse/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+
19
+ # automation-traverse
20
+
21
+ Project for setting up a graph of nodes that can be traversed as part of a
22
+ suite of automated tasks. This project builds on top of the
23
+ "automation-entities" project.
@@ -0,0 +1,5 @@
1
+ # automation-traverse
2
+
3
+ Project for setting up a graph of nodes that can be traversed as part of a
4
+ suite of automated tasks. This project builds on top of the
5
+ "automation-entities" project.
@@ -0,0 +1,48 @@
1
+ """
2
+ module pertaining to asserting
3
+ """
4
+
5
+ import pprint
6
+ from typing import Any, Dict
7
+
8
+ from automation_entities.context import Context
9
+ from automation_entities.entities import Entity
10
+
11
+
12
+ class DictAsserter(Entity):
13
+ """
14
+ asserter for asserting on ``dict`` keys and values
15
+
16
+ .. attribute:: val
17
+
18
+ ``dict`` representing the dict values
19
+ """
20
+
21
+ val: Dict[Any, Any]
22
+
23
+ def __init__(self, context: Context, val: Dict[Any, Any]) -> None:
24
+ self.val = val
25
+ super().__init__(context, pprint.pformat(val))
26
+
27
+ def get_value(self, key: Any) -> Any:
28
+ """
29
+ get and return the value corresponding to the given *key*
30
+ """
31
+ with self.interaction():
32
+ _ = self.request(f"get_value {key}")
33
+
34
+ if key not in self.val:
35
+ raise AssertionError(f"{self.val} does not contain key {key}")
36
+
37
+ with self.result() as result:
38
+ val = self.val[key]
39
+ result.log(f"Val: {pprint.pformat(val)}")
40
+
41
+ if isinstance(val, dict):
42
+ val = self.__class__(
43
+ self.context, val # pyright: ignore[reportUnknownArgumentType]
44
+ )
45
+ return val
46
+
47
+ def __getitem__(self, key: Any) -> Any:
48
+ return self.get_value(key)
@@ -0,0 +1,202 @@
1
+ from typing import IO, Any, Dict, Generic, List, Optional, Type
2
+
3
+ from automation_entities.context import Context, Subcontext
4
+ from typing_extensions import override
5
+
6
+ from .emitters import Emitter, LogLevel, T
7
+
8
+
9
+ class TraverseContext(Context, Generic[T]):
10
+ """
11
+ :class:`automation_entities.context.Context` that supports multiple
12
+ :class:`Emitter` s.
13
+ """
14
+
15
+ emitters: List[Emitter[T]]
16
+
17
+ def __init__(
18
+ self,
19
+ config_defaults: Optional[ # pyright: ignore[reportUnknownParameterType]
20
+ dict # pyright: ignore[reportMissingTypeArgument]
21
+ ] = None,
22
+ subcontext_class: Optional[Type[Subcontext]] = None,
23
+ ):
24
+ super().__init__( # pyright: ignore[reportUnknownMemberType]
25
+ config_defaults, subcontext_class
26
+ )
27
+
28
+ self.emitters = []
29
+
30
+ def add_emitter(self, emitter: Emitter[T]) -> None:
31
+ """
32
+ Add the given *emitter* to the :attr:`emitters` list.
33
+ """
34
+ self.emitters.append(emitter)
35
+
36
+ @override
37
+ def subcontext(
38
+ self, message: str, log_level: LogLevel = LogLevel.INFO
39
+ ) -> "TraverseSubcontext[T]":
40
+ """
41
+ subcontext implementation that creates a :class:`TraverseSubcontext`
42
+ subcontext rather than the original.
43
+ """
44
+ for emitter in self.emitters:
45
+ emitter.log_message(log_level, message)
46
+
47
+ return TraverseSubcontext(self, self)
48
+
49
+ def pop_subcontext(self, context_level: int) -> None:
50
+ """
51
+ Pop all emitters back to the given *context_level*.
52
+ """
53
+ for emitter in self.emitters:
54
+ emitter.pop_subcontext(context_level)
55
+
56
+ @override
57
+ def log(self, message: str) -> None:
58
+ """
59
+ Log the given *message* as info.
60
+ """
61
+ self.log_info(message)
62
+
63
+ def log_file(
64
+ self, description: str, extension: str, mode: str = "w"
65
+ ) -> "TraverseFile":
66
+ """
67
+ Log the given *description* and create a file of the given *extension*
68
+ and *mode*.
69
+ """
70
+ return TraverseFile(
71
+ [e.log_file(description, extension, mode) for e in self.emitters],
72
+ )
73
+
74
+ def log_message(self, log_level: LogLevel, message: str) -> None:
75
+ """
76
+ Log the given *message* at the given *log_level* to all configured
77
+ emitters. For more information on *log_level*, see
78
+ :meth:`emitters.Emitter.log_message`.
79
+ """
80
+ for emitter in self.emitters:
81
+ emitter.log_message(log_level, message)
82
+
83
+ def log_debug(self, message: str) -> None:
84
+ """
85
+ log the given *message* at debug level.
86
+ """
87
+ self.log_message(LogLevel.DEBUG, message)
88
+
89
+ def log_procedure(self, message: str) -> None:
90
+ """
91
+ log the given *message* at procedure level.
92
+ """
93
+ self.log_message(LogLevel.PROCEDURE, message)
94
+
95
+ def log_info(self, message: str) -> None:
96
+ """
97
+ log the given *message* at info level.
98
+ """
99
+ self.log_message(LogLevel.INFO, message)
100
+
101
+ def log_skip(self, message: str) -> None:
102
+ """
103
+ log the given *message* at skip level.
104
+ """
105
+ self.log_message(LogLevel.SKIP, message)
106
+
107
+ def log_success(self, message: str) -> None:
108
+ """
109
+ log the given *message* at success level.
110
+ """
111
+ self.log_message(LogLevel.SUCCESS, message)
112
+
113
+ def log_error(self, message: str) -> None:
114
+ """
115
+ log the given *message* at error level.
116
+ """
117
+ self.log_message(LogLevel.ERROR, message)
118
+
119
+ def log_fail(self, message: str) -> None:
120
+ """
121
+ log the given *message* at fail level.
122
+ """
123
+ self.log_message(LogLevel.FAIL, message)
124
+
125
+ def log_catastrophic(self, message: str) -> None:
126
+ """
127
+ log the given *message* at catastrophic level.
128
+ """
129
+ self.log_message(LogLevel.CATASTROPHIC, message)
130
+
131
+ def log_response(self, task: T, response: Dict[str, Any]) -> None:
132
+ """
133
+ log the given *response* for the given *task*
134
+ """
135
+ for emitter in self.emitters:
136
+ emitter.log_response(task, response)
137
+
138
+
139
+ class TraverseSubcontext(Subcontext, Generic[T]):
140
+ """
141
+ :class:`automation_entities.context.Subcontext` that supports multiple
142
+ :class:`automation_traverse.emitters.Emitter` s.
143
+ """
144
+
145
+ tranverse_context: "TraverseContext[T]"
146
+
147
+ def __init__(self, context: "Context", tranverse_context: "TraverseContext[T]"):
148
+ super().__init__(context)
149
+
150
+ self.tranverse_context = tranverse_context
151
+
152
+ @override
153
+ def __enter__(self) -> "TraverseSubcontext[T]":
154
+ _ = super().__enter__()
155
+
156
+ for emitter in self.tranverse_context.emitters:
157
+ emitter.subcontext()
158
+
159
+ return self
160
+
161
+ @override
162
+ def __exit__(self, *args: Any, **kwds: Any) -> None:
163
+ super().__exit__(*args, **kwds) # pyright: ignore[reportUnknownMemberType]
164
+
165
+ self.tranverse_context.pop_subcontext(self.tranverse_context.log_position)
166
+
167
+
168
+ class TraverseFile:
169
+ """
170
+ File-like object that multiplexes multiple file-like objects into one
171
+ object.
172
+
173
+ .. autoattribute:: fobjs
174
+ """
175
+
176
+ #: file objects wrapped by this class
177
+ fobjs: List[IO[str]]
178
+
179
+ def __init__(self, fobjs: List[IO[str]]) -> None:
180
+ self.fobjs = fobjs
181
+
182
+ def __enter__(self) -> "TraverseFile":
183
+ return self
184
+
185
+ def __exit__(self, *args: Any, **kwds: Any) -> None:
186
+ self.close()
187
+
188
+ def write(self, data: str) -> int:
189
+ """
190
+ Write *data* to all :attr:`fobjs`
191
+ """
192
+ if not self.fobjs:
193
+ return len(data)
194
+
195
+ return min(fobj.write(data) for fobj in self.fobjs)
196
+
197
+ def close(self) -> None:
198
+ """
199
+ Close all :attr:`fobjs`
200
+ """
201
+ for fobj in self.fobjs:
202
+ fobj.close()
@@ -0,0 +1,85 @@
1
+ """
2
+ module for discovering objects in Python
3
+ """
4
+
5
+ import importlib.util
6
+ import os
7
+ from types import ModuleType
8
+ from typing import Any, Dict, Iterator, Tuple, Type
9
+
10
+ from .task import Task
11
+
12
+ ImportedModuleKey = Tuple[str, str]
13
+
14
+ imported_modules: Dict[ImportedModuleKey, ModuleType] = {}
15
+
16
+
17
+ def import_module(pypath: str, filepath: str) -> ModuleType:
18
+ """
19
+ Import and return the module at the given *pypath* and *filepath*
20
+ """
21
+ key = (pypath, filepath)
22
+ if key not in imported_modules:
23
+ spec = importlib.util.spec_from_file_location(pypath, filepath)
24
+ if spec is None or spec.loader is None:
25
+ raise ValueError(f"module {key} not found")
26
+
27
+ new_module = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(new_module)
29
+ imported_modules[key] = new_module
30
+
31
+ return imported_modules[key]
32
+
33
+
34
+ def walk_modules(start_point: str) -> Iterator[Tuple[str, ModuleType]]:
35
+ """
36
+ walk over all modules from the given *start_point*
37
+ """
38
+ start_point = os.path.abspath(start_point)
39
+ for root, _, files in os.walk(start_point):
40
+ relpath = os.path.relpath(root, os.path.dirname(start_point))
41
+ module_path = tuple(relpath.split(os.path.sep))
42
+
43
+ for fname in files:
44
+ if fname == "__init__.py":
45
+ filepath = os.path.join(root, fname)
46
+ pypath = ".".join(module_path)
47
+ new_module = import_module(pypath, filepath)
48
+ yield pypath, new_module
49
+
50
+ elif fname.endswith("__.py"):
51
+ continue
52
+
53
+ elif fname.endswith(".py"):
54
+ filepath = os.path.join(root, fname)
55
+ pypath = ".".join(module_path + (fname[:-3],))
56
+ new_module = import_module(pypath, filepath)
57
+ yield pypath, new_module
58
+
59
+
60
+ def walk_object(obj: Any) -> Iterator[Any]:
61
+ """
62
+ Walk over the given object for its members
63
+ """
64
+ for attr in dir(obj):
65
+ yield attr, obj.__getattribute__(attr)
66
+
67
+
68
+ def walk_tasks(
69
+ start_point: str,
70
+ discover_all: bool = False,
71
+ ) -> Iterator[Tuple[str, str, Type[Task]]]:
72
+ """
73
+ Walk over a module and return its tasks
74
+
75
+ :param bool discover_all: whether or not to discover tasks not marked
76
+ as discoverable
77
+ """
78
+ for modulepath, module in walk_modules(start_point):
79
+ for objname, obj in walk_object(module):
80
+ if (
81
+ isinstance(obj, type)
82
+ and issubclass(obj, Task)
83
+ and (discover_all or obj.DISCOVER)
84
+ ):
85
+ yield modulepath, objname, obj
@@ -0,0 +1,3 @@
1
+ from .emitter import Emitter, LogLevel, T
2
+
3
+ __all__ = ["T", "Emitter", "LogLevel"]
@@ -0,0 +1,152 @@
1
+ import collections
2
+ import os
3
+ from enum import Enum
4
+ from typing import IO, Any, Dict, Generic, TypeVar
5
+
6
+
7
+ class LogLevel(Enum):
8
+ """
9
+ Enum indicating the different valid log levels when logging messages to an
10
+ emitter.
11
+
12
+ .. autoattribute:: DEBUG
13
+ .. autoattribute:: PROCEDURE
14
+ .. autoattribute:: INFO
15
+ .. autoattribute:: SKIP
16
+ .. autoattribute:: SUCCESS
17
+ .. autoattribute:: ERROR
18
+ .. autoattribute:: FAIL
19
+ .. autoattribute:: CATASTROPHIC
20
+ """
21
+
22
+ #: Messages meant to provide deeper insight that isn't always necessary
23
+ DEBUG = "debug"
24
+
25
+ #: Messages pertaining to the traversal procedure
26
+ PROCEDURE = "procedure"
27
+
28
+ #: Informational messages about what's happening in the task
29
+ INFO = "info"
30
+
31
+ #: Message indicating text related to a task skip
32
+ SKIP = "skip"
33
+
34
+ #: Message indicating some level of success
35
+ SUCCESS = "success"
36
+
37
+ #: Something unexpected happened which may not be recoverable
38
+ ERROR = "error"
39
+
40
+ #: A specific condition in the task has failed and is likely recoverable
41
+ FAIL = "fail"
42
+
43
+ #: Something unrecoverrable happened that requires human attention
44
+ CATASTROPHIC = "catastrophic"
45
+
46
+
47
+ T = TypeVar("T")
48
+
49
+
50
+ class Emitter(Generic[T]):
51
+ """
52
+ An emitter is an abstraction that exists to determine how various debug
53
+ information is to be passed on to the user. This is done through the
54
+ various hooks that are required to be implememented.
55
+
56
+ .. attribute:: tasks
57
+
58
+ :var:`T`\\s run under this emitter
59
+
60
+ .. attribute:: context_level
61
+
62
+ `int` context level starting from 0
63
+
64
+ .. automethod:: subcontext
65
+ .. automethod:: pop_subcontext
66
+ .. automethod:: log_message
67
+ .. automethod:: log_file
68
+ .. automethod:: finalize
69
+ """
70
+
71
+ tasks: "collections.OrderedDict[T, None]"
72
+
73
+ #: Context level starting from 0
74
+ context_level: int
75
+
76
+ def __init__(self):
77
+ self.context_level = 0
78
+ self.tasks = collections.OrderedDict()
79
+
80
+ def start_task(self, instance: T) -> None:
81
+ """
82
+ Hook that exists to inject some behavior in event of a new test
83
+ *instance* starting.
84
+ """
85
+ self.tasks[instance] = None
86
+
87
+ def end_task(self, instance: T) -> None: # pyright: ignore[reportUnusedParameter]
88
+ """
89
+ Hook that exists to inject some behavior in event of a new test
90
+ *instance* ending.
91
+ """
92
+
93
+ def subcontext(self) -> None:
94
+ """
95
+ Enter into a new subcontext. The idea behind this method is to allow
96
+ emitters to nest output inside of other output in an effort to be able
97
+ to collapse and expand text information.
98
+
99
+ If overridden by a subclass, be sure to call this original method
100
+ to retain the default functionality.
101
+ """
102
+ self.context_level += 1
103
+
104
+ def pop_subcontext(self, context_level: int) -> None:
105
+ """
106
+ Pop subcontext to the given *context_level*, which starts from 0 before
107
+ the first :meth:`subcontext` call.
108
+
109
+ If overridden by a subclass, be sure to call this original method
110
+ to retain the default functionality.
111
+ """
112
+ self.context_level = context_level
113
+
114
+ def log_message(
115
+ self,
116
+ log_level: LogLevel, # pyright: ignore[reportUnusedParameter]
117
+ message: str, # pyright: ignore[reportUnusedParameter]
118
+ end: str = "\n", # pyright: ignore[reportUnusedParameter]
119
+ ) -> None:
120
+ """
121
+ Log the given *message* at the given *log_level*. If a multi-line
122
+ message is given, it's expected to be handled appropriately by the
123
+ emitter.
124
+ """
125
+
126
+ def log_file(
127
+ self,
128
+ description: str, # pyright: ignore[reportUnusedParameter]
129
+ extension: str, # pyright: ignore[reportUnusedParameter]
130
+ mode: str = "w",
131
+ ) -> IO[str]:
132
+ """
133
+ Create a file object with the given *description* and file *extension*.
134
+ This object will be used by the caller. It's up to the caller to close
135
+ it when done.
136
+ """
137
+ return open(os.devnull, mode)
138
+
139
+ def log_response(
140
+ self,
141
+ task: T, # pyright: ignore[reportUnusedParameter]
142
+ response: Dict[str, Any], # pyright: ignore[reportUnusedParameter]
143
+ ) -> None:
144
+ """
145
+ Log the given *response* for a task.
146
+ """
147
+
148
+ def finalize(self):
149
+ """
150
+ Hook for finalizing the emitter in case there's something that needs to
151
+ be cleaned up.
152
+ """
@@ -0,0 +1,79 @@
1
+ import collections
2
+ from collections.abc import Mapping
3
+ from typing import Optional
4
+
5
+ import colorama
6
+ from typing_extensions import override
7
+
8
+ from .emitter import Emitter, LogLevel, T
9
+
10
+
11
+ class SimpleLogEmitter(Emitter[T]):
12
+ """
13
+ The SimpleLogEmitter is an abstract :class:`Emitter` implementation that
14
+ allows for subclasses to simply implement a single :meth:`emit` method
15
+ to determine where the data goes. The *use_color* argument defaults to
16
+ ``True`` and determines whether or not log messages should be emitted
17
+ with color encoding.
18
+
19
+ :param bool use_color: whether or not to output in color (default
20
+ ``True``)
21
+ :param int context_level_spaces: number of spaces per context level
22
+ (default 4)
23
+
24
+ .. automethod:: emit
25
+ """
26
+
27
+ use_color: bool
28
+ context_level_spaces: int
29
+
30
+ DEFAULT_CONTEXT_LEVEL_SPACES: int = 4
31
+
32
+ LOG_LEVEL_COLORS: Mapping[LogLevel, str] = collections.defaultdict(
33
+ lambda: colorama.Style.RESET_ALL,
34
+ {
35
+ LogLevel.DEBUG: colorama.Fore.WHITE + colorama.Style.DIM,
36
+ LogLevel.PROCEDURE: colorama.Fore.BLUE,
37
+ LogLevel.INFO: colorama.Fore.WHITE,
38
+ LogLevel.SKIP: colorama.Fore.MAGENTA + colorama.Style.BRIGHT,
39
+ LogLevel.SUCCESS: colorama.Fore.GREEN,
40
+ LogLevel.ERROR: colorama.Fore.RED + colorama.Style.BRIGHT,
41
+ LogLevel.FAIL: colorama.Fore.RED,
42
+ LogLevel.CATASTROPHIC: colorama.Fore.CYAN + colorama.Style.BRIGHT,
43
+ },
44
+ )
45
+
46
+ def __init__(
47
+ self,
48
+ use_color: bool = True,
49
+ context_level_spaces: Optional[int] = None,
50
+ ):
51
+ super().__init__()
52
+
53
+ self.use_color = use_color
54
+ self.context_level_spaces = (
55
+ context_level_spaces or self.DEFAULT_CONTEXT_LEVEL_SPACES
56
+ )
57
+
58
+ @override
59
+ def log_message(self, log_level: LogLevel, message: str, end: str = "\n") -> None:
60
+ """
61
+ Specific information of :meth:`Emitter.log_message` that calls
62
+ :meth:`emit` with color-encoded text.
63
+ """
64
+ spaces = " " * (self.context_level * self.context_level_spaces)
65
+ color_pre_tag = ""
66
+ color_post_tag = ""
67
+ if self.use_color:
68
+ color_pre_tag = self.LOG_LEVEL_COLORS[log_level]
69
+ color_post_tag = colorama.Style.RESET_ALL
70
+
71
+ for line in message.splitlines() if end == "\n" else message.split(end):
72
+ line = line.rstrip()
73
+ self.emit(f"{spaces}{color_pre_tag}{line}{color_post_tag}{end}")
74
+
75
+ def emit(self, message: str) -> None: # pyright: ignore[reportUnusedParameter]
76
+ """
77
+ Emit the given *message*. This should be implemented by sub-class
78
+ implementations.
79
+ """
@@ -0,0 +1,67 @@
1
+ import io
2
+ import pprint
3
+ import sys
4
+ from typing import Any, Dict, Optional, TextIO, Union
5
+
6
+ from typing_extensions import override
7
+
8
+ from .emitter import LogLevel, T
9
+ from .simple_log import SimpleLogEmitter
10
+
11
+
12
+ class TerminalEmitter(SimpleLogEmitter[T]):
13
+ """
14
+ :class:`SimpleLogEmitter` implementation that logs output to the terminal
15
+ or to the given *fobj*.
16
+
17
+ :param bool use_color: whether or not to output in color (default
18
+ ``True``)
19
+ :param bool flush: whether or not to flush to the file after every
20
+ message
21
+ :param fobj: file-like object to log to (default :data:`sys.stdout`)
22
+ :param int context_level_spaces: number of spaces per context level
23
+ (default 4)
24
+ """
25
+
26
+ flush: bool
27
+ fobj: Union[io.IOBase, TextIO]
28
+
29
+ def __init__(
30
+ self,
31
+ use_color: bool = True,
32
+ flush: bool = True,
33
+ fobj: Optional[io.IOBase] = None,
34
+ context_level_spaces: Optional[int] = None,
35
+ ):
36
+ super().__init__(
37
+ use_color=use_color,
38
+ context_level_spaces=context_level_spaces,
39
+ )
40
+
41
+ self.flush = flush
42
+ self.fobj = fobj or sys.stdout
43
+
44
+ @override
45
+ def emit(self, message: str) -> None:
46
+ """
47
+ Implementation of :meth:`SimpleLogEmitter.emit` that emits the given
48
+ *message* text to the configured :attr:`fobj`.
49
+ """
50
+ _ = self.fobj.write(message)
51
+ if self.flush:
52
+ self.fobj.flush()
53
+
54
+ @override
55
+ def log_response(self, task: T, response: Dict[str, Any]) -> None:
56
+ """
57
+ Log the given *response* for a task.
58
+ """
59
+ self.log_message(
60
+ LogLevel.PROCEDURE,
61
+ "\n".join(
62
+ [
63
+ "Response for task:",
64
+ pprint.pformat(response),
65
+ ]
66
+ ),
67
+ )