libsrg 4.4.1__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 (46) hide show
  1. libsrg-4.4.1/LICENSE.txt +21 -0
  2. libsrg-4.4.1/PKG-INFO +106 -0
  3. libsrg-4.4.1/README.md +81 -0
  4. libsrg-4.4.1/libsrg/AppTemplate.py +47 -0
  5. libsrg-4.4.1/libsrg/Config.py +284 -0
  6. libsrg-4.4.1/libsrg/ElapsedTime.py +90 -0
  7. libsrg-4.4.1/libsrg/Info.py +222 -0
  8. libsrg-4.4.1/libsrg/LevelBanner.py +116 -0
  9. libsrg-4.4.1/libsrg/LoggerGUIProxy.py +46 -0
  10. libsrg-4.4.1/libsrg/LoggingAppBase.py +137 -0
  11. libsrg-4.4.1/libsrg/LoggingCounter.py +178 -0
  12. libsrg-4.4.1/libsrg/LoggingUtils.py +20 -0
  13. libsrg-4.4.1/libsrg/LoggingWatcher.py +44 -0
  14. libsrg-4.4.1/libsrg/NagiosBase.py +82 -0
  15. libsrg-4.4.1/libsrg/Runner.py +185 -0
  16. libsrg-4.4.1/libsrg/Runner2.py +111 -0
  17. libsrg-4.4.1/libsrg/Statistics/ADStatsBase.py +133 -0
  18. libsrg-4.4.1/libsrg/Statistics/AnalogStatsBase.py +76 -0
  19. libsrg-4.4.1/libsrg/Statistics/AnalogStatsCumulative.py +50 -0
  20. libsrg-4.4.1/libsrg/Statistics/AnalogStatsFading.py +59 -0
  21. libsrg-4.4.1/libsrg/Statistics/AnalogStatsSlidingWindow.py +48 -0
  22. libsrg-4.4.1/libsrg/Statistics/DiscreteStatsBase.py +51 -0
  23. libsrg-4.4.1/libsrg/Statistics/DiscreteStatsCumulative.py +26 -0
  24. libsrg-4.4.1/libsrg/Statistics/DiscreteStatsSlidingWindow.py +49 -0
  25. libsrg-4.4.1/libsrg/Statistics/UnitTests/ADStatsBase_test.py +72 -0
  26. libsrg-4.4.1/libsrg/Statistics/UnitTests/AnalogStatsBase_test.py +114 -0
  27. libsrg-4.4.1/libsrg/Statistics/UnitTests/AnalogStatsCumulative_test.py +77 -0
  28. libsrg-4.4.1/libsrg/Statistics/UnitTests/AnalogStatsFading_test.py +116 -0
  29. libsrg-4.4.1/libsrg/Statistics/UnitTests/AnalogStatsSlidingWindow_test.py +96 -0
  30. libsrg-4.4.1/libsrg/Statistics/UnitTests/DiscteteStatsBase_test.py +55 -0
  31. libsrg-4.4.1/libsrg/Statistics/UnitTests/DiscteteStatsSlidingWindow_test.py +80 -0
  32. libsrg-4.4.1/libsrg/Statistics/UnitTests/__init__.py +0 -0
  33. libsrg-4.4.1/libsrg/Statistics/__init__.py +0 -0
  34. libsrg-4.4.1/libsrg/TKGUI/GuiBase.py +70 -0
  35. libsrg-4.4.1/libsrg/TKGUI/GuiRequest.py +18 -0
  36. libsrg-4.4.1/libsrg/TKGUI/GuiRequestQueue.py +107 -0
  37. libsrg-4.4.1/libsrg/TKGUI/LoggerGUI.py +318 -0
  38. libsrg-4.4.1/libsrg/TKGUI/__init__.py +1 -0
  39. libsrg-4.4.1/libsrg/UnitTests/Config_test.py +164 -0
  40. libsrg-4.4.1/libsrg/UnitTests/RolloverTest_filename.py +47 -0
  41. libsrg-4.4.1/libsrg/UnitTests/RolloverTest_logfile.py +47 -0
  42. libsrg-4.4.1/libsrg/UnitTests/Sample.env +3 -0
  43. libsrg-4.4.1/libsrg/UnitTests/Sample.ini +9 -0
  44. libsrg-4.4.1/libsrg/UnitTests/Sample.json +5 -0
  45. libsrg-4.4.1/libsrg/__init__.py +0 -0
  46. libsrg-4.4.1/pyproject.toml +34 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
libsrg-4.4.1/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.1
2
+ Name: libsrg
3
+ Version: 4.4.1
4
+ Summary: Utility lib logging, statistics, subprocesses
5
+ Author: Steve Goncalo
6
+ Author-email: steven@goncalo.us
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Utilities
19
+ Provides-Extra: doc
20
+ Requires-Dist: jinja2 (>=3.1.3,<4.0.0)
21
+ Requires-Dist: sphinx-rtd-theme[doc] (>=2.0.0,<3.0.0) ; extra == "doc"
22
+ Requires-Dist: sphinx[doc] (>=7.3.7,<8.0.0) ; extra == "doc"
23
+ Requires-Dist: sphinxcontrib-napoleon[doc] (>=0.7,<0.8) ; extra == "doc"
24
+ Description-Content-Type: text/markdown
25
+
26
+ ## Name
27
+ libsrg -- count calls to python logging at each logging severity level
28
+
29
+ ## Description
30
+ libsrg provides the following classes:
31
+ * libsrg.LoggingCounter
32
+ * extends logging.Handler with count of logs at each severity level
33
+ * counts on output, so logs suppressed by current logging level do not get counted
34
+ * atexit hook used to print statistics on program exit
35
+ * libsrg.LoggingAppBase
36
+ * application base class with LoggingCounter installed
37
+ * argparse.ArgumentParser
38
+ * --verbose sets logging level to DEBUG
39
+ * --logfile <file> directs logging to a file
40
+ * Derived classes can add application specific options to ArgumentParser
41
+ * libsrg.Runner
42
+ * run command line in a subprocess
43
+ * capture stdout, stderr
44
+ * try catch block for exceptions
45
+ * exceptions can optionally be raised back to calling program
46
+
47
+ I write a lot of smallish command line utilities for internal use, and factored out some common
48
+ repetitive boilerplate code into this package. Published for my own installation convenience,
49
+ but might be useful to others as well.
50
+
51
+ ## Example output
52
+
53
+ Library uses atexit hook to summarize logging activity counts and total execution time.
54
+ Counts are also available to the program at runtime.
55
+ ```
56
+ 2022-02-22 12:46:22,568 INFO (libsrg.LoggingCounter:61) __log_atexit
57
+ Logging Summary:
58
+ Logging at Level INFO occurred 13 times
59
+ Logging at Level DEBUG occurred 1 times
60
+ Logging at Level WARNING occurred 1 times
61
+ Logging at Level ERROR occurred 2 times
62
+ Logging at Level CRITICAL occurred 1 times
63
+ Elapsed time was 0.087 seconds
64
+ ```
65
+
66
+ ## Internal notes on updating
67
+
68
+ * /GNAS/PROJ/PycharmProjects/libsrg/publish.bash
69
+ ```
70
+ #! /bin/bash
71
+
72
+ cd /GNAS/PROJ/PycharmProjects/libsrg
73
+ rm -rf dist
74
+ python3 -m build --wheel
75
+ python3 -m twine check dist/*
76
+ python3 -m twine upload dist/*
77
+ ```
78
+
79
+ ## Installation from pypi via pip
80
+ * pip3 install libsrg
81
+ * pip3 install libsrg --update
82
+
83
+ ## Installation from Gitlab or local repository
84
+ * pip3 install git+ssh://git@gitlab.com/SRG_gitlab/libsrg.git
85
+ * pip3 install /GNAS/PROJ/PycharmProjects/libsrg
86
+
87
+ ## Testing
88
+
89
+ Pytest performs its own logging setup before calling any user supplied tests, so it
90
+ doesn't work well testing these classes logging setup. Ad-hoc test code is supplied
91
+ at the end of the classes and executes if the class is loaded as __main__.
92
+
93
+
94
+ ## Roadmap
95
+ My intention is to keep this library small. It is not expected to evolve into an all-purpose framework.
96
+ Output is geared towards developers, not end users.
97
+
98
+ ## License
99
+ MIT
100
+
101
+ ## Project status
102
+ Core functionality appears solid, but naming and packaging may be fluid for a while.
103
+
104
+ This is my first attempt at publishing to pypi, so there may be surprises.
105
+
106
+ I will bump version to 1.x when out of beta.
libsrg-4.4.1/README.md ADDED
@@ -0,0 +1,81 @@
1
+ ## Name
2
+ libsrg -- count calls to python logging at each logging severity level
3
+
4
+ ## Description
5
+ libsrg provides the following classes:
6
+ * libsrg.LoggingCounter
7
+ * extends logging.Handler with count of logs at each severity level
8
+ * counts on output, so logs suppressed by current logging level do not get counted
9
+ * atexit hook used to print statistics on program exit
10
+ * libsrg.LoggingAppBase
11
+ * application base class with LoggingCounter installed
12
+ * argparse.ArgumentParser
13
+ * --verbose sets logging level to DEBUG
14
+ * --logfile <file> directs logging to a file
15
+ * Derived classes can add application specific options to ArgumentParser
16
+ * libsrg.Runner
17
+ * run command line in a subprocess
18
+ * capture stdout, stderr
19
+ * try catch block for exceptions
20
+ * exceptions can optionally be raised back to calling program
21
+
22
+ I write a lot of smallish command line utilities for internal use, and factored out some common
23
+ repetitive boilerplate code into this package. Published for my own installation convenience,
24
+ but might be useful to others as well.
25
+
26
+ ## Example output
27
+
28
+ Library uses atexit hook to summarize logging activity counts and total execution time.
29
+ Counts are also available to the program at runtime.
30
+ ```
31
+ 2022-02-22 12:46:22,568 INFO (libsrg.LoggingCounter:61) __log_atexit
32
+ Logging Summary:
33
+ Logging at Level INFO occurred 13 times
34
+ Logging at Level DEBUG occurred 1 times
35
+ Logging at Level WARNING occurred 1 times
36
+ Logging at Level ERROR occurred 2 times
37
+ Logging at Level CRITICAL occurred 1 times
38
+ Elapsed time was 0.087 seconds
39
+ ```
40
+
41
+ ## Internal notes on updating
42
+
43
+ * /GNAS/PROJ/PycharmProjects/libsrg/publish.bash
44
+ ```
45
+ #! /bin/bash
46
+
47
+ cd /GNAS/PROJ/PycharmProjects/libsrg
48
+ rm -rf dist
49
+ python3 -m build --wheel
50
+ python3 -m twine check dist/*
51
+ python3 -m twine upload dist/*
52
+ ```
53
+
54
+ ## Installation from pypi via pip
55
+ * pip3 install libsrg
56
+ * pip3 install libsrg --update
57
+
58
+ ## Installation from Gitlab or local repository
59
+ * pip3 install git+ssh://git@gitlab.com/SRG_gitlab/libsrg.git
60
+ * pip3 install /GNAS/PROJ/PycharmProjects/libsrg
61
+
62
+ ## Testing
63
+
64
+ Pytest performs its own logging setup before calling any user supplied tests, so it
65
+ doesn't work well testing these classes logging setup. Ad-hoc test code is supplied
66
+ at the end of the classes and executes if the class is loaded as __main__.
67
+
68
+
69
+ ## Roadmap
70
+ My intention is to keep this library small. It is not expected to evolve into an all-purpose framework.
71
+ Output is geared towards developers, not end users.
72
+
73
+ ## License
74
+ MIT
75
+
76
+ ## Project status
77
+ Core functionality appears solid, but naming and packaging may be fluid for a while.
78
+
79
+ This is my first attempt at publishing to pypi, so there may be surprises.
80
+
81
+ I will bump version to 1.x when out of beta.
@@ -0,0 +1,47 @@
1
+ import logging
2
+
3
+ from libsrg.LoggingAppBase import LoggingAppBase
4
+
5
+ """
6
+ This module is a sample application template for libsrg application logging
7
+ """
8
+
9
+
10
+ class Other:
11
+ def __init__(self, n):
12
+ self.logger = logging.getLogger(self.__class__.__name__)
13
+ self.logger.info(f"Created Other {n}!")
14
+
15
+
16
+ class SampleApp(LoggingAppBase):
17
+ """
18
+ SampleApp extends LoggingAppBase to provide sample application.
19
+ """
20
+ def __init__(self):
21
+ """
22
+ Constructor
23
+ * must call super().__init__()
24
+ * initializes logging
25
+ * can then add arguments to ArgumentParser defined by base in self.parser
26
+ * arguments are available at self.args after call to self.perform_parse()
27
+ """
28
+ super().__init__() # super defines self.logger
29
+ self.logger.info("before adding args")
30
+ # setup any program specific command line arguments
31
+ self.parser.add_argument('--zap', help="Zap something", dest='zap', action='store_true', default=False)
32
+ self.parser.add_argument('--zip', help="Zip something", dest='zip', action='store_true', default=False)
33
+ # invoke the parser
34
+ self.perform_parse()
35
+ #
36
+ self.logger.info(f"after parsing {self.args}")
37
+
38
+ @classmethod
39
+ def demo(cls):
40
+ """simple demo of main and helper classes"""
41
+ _ = SampleApp()
42
+ _ = Other(1)
43
+ _ = Other(2)
44
+
45
+
46
+ if __name__ == '__main__':
47
+ SampleApp.demo()
@@ -0,0 +1,284 @@
1
+ import configparser
2
+ import json
3
+ import os
4
+ import re
5
+ from collections import ChainMap
6
+ from pathlib import Path
7
+ from typing import Any, Self, Optional, Tuple, TypeAlias
8
+
9
+ from jinja2 import Template
10
+
11
+ """FileName can be a str, bytes or a pathlib.Path."""
12
+ FileName: TypeAlias = str | bytes | os.PathLike
13
+ """ConfigSource can be a ChainMap, a dict, or a FileName as defined above."""
14
+ ConfigSource: TypeAlias = ChainMap[str, Any] | dict[str, Any] | FileName
15
+
16
+
17
+ class Config(ChainMap):
18
+ """
19
+ The Config class extends the ChainMap class to provide access to config files.
20
+
21
+ From outside, a Config looks like a series of python dictionaries which get searched in
22
+ order for a given configuation item,
23
+
24
+ Jinja argument processing can be applied on a per item basis using the get_item function,
25
+ Jinja processing is NOT applied as files are read in from the config file. Other items in the config
26
+ can be referenced in jinja templates.
27
+
28
+ The Config class maintains one special Config instance which contains secrets. These can be usernames,
29
+ passwords, api keys, or anything else that should not normally be kept in publicly accessible
30
+ configuration files. One file of secrets can be read from a secure location and referenced by items
31
+ in the normal config files.
32
+
33
+ """
34
+ _secret_config: Self | None = None
35
+
36
+ @classmethod
37
+ def set_secrets(cls, *args: ConfigSource) -> None:
38
+ """
39
+ Sets the secret config held by this class.
40
+ :param args: zero or more Config, ChainMap, dict[str, Any], Path
41
+ :return: None
42
+ """
43
+ cls._secret_config = cls(*args)
44
+
45
+ def __init__(self, *args: ConfigSource):
46
+ """
47
+ Constructs a new Config object.
48
+ :param args: zero or more Config, ChainMap, dict[str, Any], Path
49
+ """
50
+ args2 = self.process_args(*args)
51
+ super().__init__(*args2)
52
+
53
+ @classmethod
54
+ def process_args(cls, *args: ConfigSource) -> list[dict[str, Any]]:
55
+ """
56
+ Processes a mixed list of Config like inputs to a list of dictionaries.
57
+ * Paths or str filenames are opened and loaded from disk.
58
+ * Config or ChainMap inputs are split into their internal maps (preserving order)
59
+ :param args: zero or more Config, ChainMap, dict[str, Any], Path
60
+ :return: an ordered list of dict objects
61
+ """
62
+ pending: list[Any] = list(args)
63
+ out: list[dict[str, Any]] = []
64
+ while len(pending) > 0:
65
+ candidate = pending.pop(0)
66
+
67
+ # for ChainMap/Config inputs, add the individual maps
68
+ if isinstance(candidate, ChainMap):
69
+ maps = candidate.maps.copy() # dont consume the source!
70
+ while len(maps) > 0:
71
+ amap = maps.pop(-1)
72
+ pending.insert(0, amap)
73
+ continue
74
+
75
+ # assume string argument is a pathname
76
+ if isinstance(candidate, str):
77
+ candidate = Path(candidate)
78
+
79
+ # open path arguments
80
+ if isinstance(candidate, Path):
81
+ with open(candidate, "r") as f:
82
+ text = f.read()
83
+ # noinspection PyBroadException
84
+ candidate = cls.text_to_dict(text)
85
+
86
+ # only valid choice is dict here
87
+ if isinstance(candidate, dict):
88
+ out.append(candidate)
89
+ else:
90
+ raise TypeError(f"{type(candidate)} unexpected at {candidate}")
91
+ return out
92
+
93
+ @classmethod
94
+ def text_to_dict(cls, text: str) -> dict[str, Any]:
95
+ """
96
+ Converts a string into a dict. Content type is determined using a few heuristics.
97
+ * convert from json if a line starting with "{" is found
98
+ * convert from yaml if a line starting with "---" is found TODO #36
99
+ * convert from ini/configparser if a line starting with "[" is found
100
+ * note first level in dict will be sections, not items
101
+ * convert from env/bash name=value otherwise
102
+ :param text:
103
+ :return: ict[str, Any]
104
+ """
105
+ text = cls.strip_comments(text)
106
+ json_pat = re.compile(r"^\s*\{", re.MULTILINE)
107
+ if json_pat.match(text):
108
+ out = json.loads(text)
109
+ return out
110
+ ini_pat = re.compile(r"^\s*\[", re.MULTILINE)
111
+ if ini_pat.match(text):
112
+ cp = configparser.ConfigParser()
113
+ cp.read_string(text)
114
+ # reformat ini data as native dict of dicts
115
+ out = {name: {k: v for k, v in cp[name].items()} for name in cp.sections()}
116
+ return out
117
+ # fallback to flat env like file w/o [inisection]
118
+ cp = configparser.ConfigParser()
119
+ cp.read_string("[qqq]\n" + text)
120
+ out = {k: v for k, v in cp["qqq"].items()}
121
+ return out
122
+
123
+ @classmethod
124
+ def text_to_config(cls, text: str) -> Self:
125
+ """Create a Config instance from a string (see text_to_dict)."""
126
+ return cls(cls.text_to_dict(text))
127
+
128
+ @classmethod
129
+ def strip_comments(cls, text: str, markers: Tuple[str] = None) -> str:
130
+ """
131
+ Strips line comments starting with markers
132
+ :param markers: tuple of line markers, defaults to ("#", ";", "//")
133
+ :param text: input text
134
+ :return: output text with line comments removed
135
+ """
136
+ if markers is None:
137
+ markers = ("#", ";", "//")
138
+ lines = text.split("\n")
139
+ new_lines = [line for line in lines if not line.strip(" \t").startswith(markers)]
140
+ return "\n".join(new_lines)
141
+
142
+ def get_item(self, *item_names: str, default=None, allow_none=False,
143
+ secrets: bool = False, jinja=True) -> Any:
144
+ """
145
+ get an argument from self.merged_args
146
+ :param item_names: one or more alternate names, first found used
147
+ :param default: if none of the names are found return this value
148
+ :param allow_none: set True to suppress KeyError if returning None
149
+ :param jinja: enable jinja processing on item read from config
150
+ :param secrets: enable secrets in config chaining
151
+ :return: first item found or None if allow_none is set
152
+
153
+ This getter is designed to support gradual switchover from legacy names to newer names.
154
+
155
+ As a special case, if an Exception type is placed in the chain of adaption data, it will get raised.
156
+ This is intended to facilitate marking required parameters in default config data.
157
+ """
158
+ val = default
159
+ config = self
160
+ if secrets and self._secret_config is not None:
161
+ config = ChainMap(config, self._secret_config)
162
+ for name in item_names:
163
+ if name in config:
164
+ val = config[name]
165
+ break
166
+ if val is None and not allow_none:
167
+ raise KeyError(f"keys not found: {item_names}")
168
+ if isinstance(val, Exception):
169
+ raise val
170
+ if jinja or secrets:
171
+ # config already has secrets if enabled
172
+ val = self.apply_templates_to_strings(val, config=config, secrets=False)
173
+ return val
174
+
175
+ def apply_templates_to_strings(self, data: Any, config: Optional[Self] = None,
176
+ secrets: bool = False) -> Any:
177
+ """
178
+ apply templates to strings in data (applies to str or list of str, else return unchanged
179
+ * if data is a str, apply templates
180
+ * if data is a list containing ONLY str, apply recursively for one level only
181
+ * anything else returns unmodified
182
+ :param data: data to be processed
183
+ :param config: defaults to self, if caller has already appended secrets, that can be passed here
184
+ :param secrets: append secrets before templates
185
+ :return: result
186
+ """
187
+ if config is None:
188
+ config = self
189
+ if secrets:
190
+ config = ChainMap(config, self._secret_config)
191
+ if isinstance(data, str):
192
+ template = Template(data)
193
+ val = template.render(config)
194
+ return val
195
+ if isinstance(data, list):
196
+ # only apply to list of str, not list of Any
197
+ if all([isinstance(x, str) for x in data]):
198
+ return [self.apply_templates_to_strings(x, config=config, secrets=False) for x in data]
199
+ return data
200
+
201
+ def set_item_if_missing(self, key: str, val: Any) -> None:
202
+ """
203
+ Convenience method for set_item which prevents overwriting existing item value
204
+ :param key: name of item
205
+ :param val: value of item
206
+ :return: None
207
+ """
208
+ self.set_item(key, val, overwrite=False)
209
+
210
+ def set_item(self, key: str, value: Any, overwrite=True, ) -> None:
211
+ """
212
+ Set one item in config
213
+ :param key: item name
214
+ :param value: item value
215
+ :param overwrite: Allow overwriting existing item (defaults True)
216
+ :return: None
217
+ """
218
+ if key not in self or overwrite:
219
+ self[key] = value
220
+
221
+ def set_items(self, overwrite: bool = True, **key_value_pairs) -> None:
222
+ """Set one or more items in config using key/value pairs
223
+ :param overwrite: Allow overwriting existing item (defaults True)
224
+ :param key_value_pairs: key/value pairs to be set
225
+ :return: None
226
+ """
227
+ for key, val in key_value_pairs.items():
228
+ self.set_item(key, val, overwrite=overwrite)
229
+
230
+ def find_item_depth(self, item: str) -> Tuple[Optional[int], Any]:
231
+ """Looks for item in maps
232
+ :param item: item name
233
+ :return: (depth, value) if found tuple with depth and value, else (None,None)
234
+ """
235
+ for depth, dct in enumerate(self.maps):
236
+ if item in dct:
237
+ return depth, dct[item]
238
+ return None, None
239
+
240
+ def to_list(self) -> list[tuple[str, Any, int]]:
241
+ """Finds and alphabetizes all config items in config
242
+ :return: Ordered list of tuples (name, value, depth)
243
+ """
244
+ return self.config_to_list(self)
245
+
246
+ @classmethod
247
+ def config_to_list(cls, config: Self) -> list[tuple[str, Any, int]]:
248
+ """Finds and alphabetizes all config items in config
249
+ :param config: Config instance
250
+ :return: Ordered list of tuples (name, value, depth)
251
+ """
252
+ names = list(config.keys())
253
+ names.sort()
254
+ lst = []
255
+ for name in names:
256
+ depth, val = config.find_item_depth(name)
257
+ lst.append((name, val, depth))
258
+ return lst
259
+
260
+ def to_json_str(self, **kwargs) -> str:
261
+ """
262
+ Produces a flat dict from a Config and converts to JSON string.
263
+ :param kwargs: Keyword arguments pased to json.dump
264
+ :return: JSON string of Config
265
+ """
266
+ return json.dumps(self.to_flat_dict(), **kwargs)
267
+
268
+ def to_json_file(self, file: FileName, **kwargs):
269
+ """
270
+ Produces a flat dict from a Config and converts to JSON file.
271
+ :param file: str or Path instance
272
+ :param kwargs: keyword arguments pased to json.dump
273
+ :return: None
274
+ """
275
+ txt = self.to_json_str(**kwargs)
276
+ with open(file, 'w') as f:
277
+ f.write(txt)
278
+
279
+ def to_flat_dict(self, ) -> dict[str, Any]:
280
+ """
281
+ Produces a flat dict from a Config.
282
+ :return: dict contents of Config
283
+ """
284
+ return dict(self)
@@ -0,0 +1,90 @@
1
+ """
2
+ ElapsedTime is a class to measure elapsed time between start and stop events.
3
+ It can be operated directly using start and stop methods, or it can
4
+ act as a context manager in a "with" statement block.
5
+ """
6
+ import logging
7
+ from datetime import timedelta
8
+ from time import time
9
+
10
+
11
+ class ElapsedTime:
12
+
13
+ def __init__(self, name=None):
14
+ """
15
+ Constructs ElapsedTime object, started
16
+ :param name: Name of ElapsedTime
17
+ """
18
+ self.logger = logging.getLogger(self.__class__.__name__)
19
+ if name:
20
+ self.name = name
21
+ else:
22
+ self.name = repr(self)
23
+ self._starttime = 0
24
+ self._endtime = 0
25
+ self._elapsed = 0
26
+ self.start()
27
+
28
+ def elapsed_asc(self) -> str:
29
+ """
30
+ Returns elapsed time as formatted string.
31
+ Note elapsed time is computed when stop() is called.
32
+ Use current() to get the current elapsed time without stopping.
33
+ :return:
34
+ """
35
+ td = timedelta(seconds=self._elapsed)
36
+ return str(td)
37
+
38
+ def __str__(self):
39
+ """
40
+ Returns name and elapsed time as formatted string.
41
+ :return:
42
+ """
43
+ return f"ET({self.name!r},{self.elapsed_asc()},{self._elapsed})"
44
+
45
+
46
+
47
+ def start(self):
48
+ """
49
+ records start time and zeros elapsed.
50
+ * if called more than once, last call wipes any previous data
51
+ * constructor also starts recording elapsed time, so this is only needed to restart timing
52
+
53
+ """
54
+ self._starttime = time()
55
+ self._endtime = self._starttime
56
+ self._elapsed = 0
57
+
58
+
59
+ def stop(self) -> float:
60
+ """records stop time, computes and returns elapsed in seconds
61
+ if called more than once, each call records time since last start
62
+ :return: elapsed time in seconds
63
+ """
64
+ self._endtime = time()
65
+ self._elapsed = self._endtime - self._starttime
66
+ return self._elapsed
67
+
68
+
69
+ def elapsed(self) -> float:
70
+ """returns elapsed time at last stop, but does not perform a stop
71
+ :return: elapsed time in seconds
72
+ """
73
+ return self._elapsed
74
+
75
+
76
+ def current(self) -> float:
77
+ """returns time since last start, but does not perform a stop
78
+ :return: elapsed time in seconds
79
+ """
80
+ return time() - self._starttime
81
+
82
+
83
+ def __enter__(self):
84
+ """translates enter "with" statement into start command"""
85
+ self.start()
86
+
87
+
88
+ def __exit__(self, exc_type, exc_value, exc_tb):
89
+ """translates exit "with" statement into stop command"""
90
+ self.stop()