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