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.
- libsrg-4.4.0/LICENSE.txt +21 -0
- libsrg-4.4.0/PKG-INFO +102 -0
- libsrg-4.4.0/README.md +81 -0
- libsrg-4.4.0/libsrg/AppTemplate.py +37 -0
- libsrg-4.4.0/libsrg/Config.py +266 -0
- libsrg-4.4.0/libsrg/ElapsedTime.py +66 -0
- libsrg-4.4.0/libsrg/Info.py +181 -0
- libsrg-4.4.0/libsrg/LevelBanner.py +116 -0
- libsrg-4.4.0/libsrg/LoggerGUIProxy.py +46 -0
- libsrg-4.4.0/libsrg/LoggingAppBase.py +137 -0
- libsrg-4.4.0/libsrg/LoggingCounter.py +178 -0
- libsrg-4.4.0/libsrg/LoggingUtils.py +20 -0
- libsrg-4.4.0/libsrg/LoggingWatcher.py +44 -0
- libsrg-4.4.0/libsrg/NagiosBase.py +82 -0
- libsrg-4.4.0/libsrg/ReZFS.py +356 -0
- libsrg-4.4.0/libsrg/Runner.py +163 -0
- libsrg-4.4.0/libsrg/Runner2.py +111 -0
- libsrg-4.4.0/libsrg/Stage0.py +176 -0
- libsrg-4.4.0/libsrg/Statistics/ADStatsBase.py +133 -0
- libsrg-4.4.0/libsrg/Statistics/AnalogStatsBase.py +76 -0
- libsrg-4.4.0/libsrg/Statistics/AnalogStatsCumulative.py +50 -0
- libsrg-4.4.0/libsrg/Statistics/AnalogStatsFading.py +59 -0
- libsrg-4.4.0/libsrg/Statistics/AnalogStatsSlidingWindow.py +48 -0
- libsrg-4.4.0/libsrg/Statistics/DiscreteStatsBase.py +51 -0
- libsrg-4.4.0/libsrg/Statistics/DiscreteStatsCumulative.py +26 -0
- libsrg-4.4.0/libsrg/Statistics/DiscreteStatsSlidingWindow.py +49 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/ADStatsBase_test.py +72 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/AnalogStatsBase_test.py +114 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/AnalogStatsCumulative_test.py +77 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/AnalogStatsFading_test.py +116 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/AnalogStatsSlidingWindow_test.py +96 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/DiscteteStatsBase_test.py +55 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/DiscteteStatsSlidingWindow_test.py +80 -0
- libsrg-4.4.0/libsrg/Statistics/UnitTests/__init__.py +0 -0
- libsrg-4.4.0/libsrg/Statistics/__init__.py +0 -0
- libsrg-4.4.0/libsrg/TKGUI/GuiBase.py +70 -0
- libsrg-4.4.0/libsrg/TKGUI/GuiRequest.py +18 -0
- libsrg-4.4.0/libsrg/TKGUI/GuiRequestQueue.py +107 -0
- libsrg-4.4.0/libsrg/TKGUI/LoggerGUI.py +318 -0
- libsrg-4.4.0/libsrg/TKGUI/__init__.py +1 -0
- libsrg-4.4.0/libsrg/UnitTests/Config_test.py +164 -0
- libsrg-4.4.0/libsrg/UnitTests/RolloverTest_filename.py +47 -0
- libsrg-4.4.0/libsrg/UnitTests/RolloverTest_logfile.py +47 -0
- libsrg-4.4.0/libsrg/UnitTests/Sample.env +3 -0
- libsrg-4.4.0/libsrg/UnitTests/Sample.ini +9 -0
- libsrg-4.4.0/libsrg/UnitTests/Sample.json +5 -0
- libsrg-4.4.0/libsrg/__init__.py +0 -0
- libsrg-4.4.0/libsrg/template.py +37 -0
- libsrg-4.4.0/libsrg/ztool.py +620 -0
- libsrg-4.4.0/pyproject.toml +29 -0
libsrg-4.4.0/LICENSE.txt
ADDED
|
@@ -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()
|