devopsdriver 0.1.32__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.
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.1
2
+ Name: devopsdriver
3
+ Version: 0.1.32
4
+ Summary: DevOps tools
5
+ Author-email: Marc Page <marcallenpage@gmail.com>
6
+ License: This is free and unencumbered software released into the public domain.
7
+
8
+ Anyone is free to copy, modify, publish, use, compile, sell, or
9
+ distribute this software, either in source code form or as a compiled
10
+ binary, for any purpose, commercial or non-commercial, and by any
11
+ means.
12
+
13
+ In jurisdictions that recognize copyright laws, the author or authors
14
+ of this software dedicate any and all copyright interest in the
15
+ software to the public domain. We make this dedication for the benefit
16
+ of the public at large and to the detriment of our heirs and
17
+ successors. We intend this dedication to be an overt act of
18
+ relinquishment in perpetuity of all present and future rights to this
19
+ software under copyright law.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ For more information, please refer to <https://unlicense.org>
30
+
31
+ Project-URL: Homepage, https://github.com/marcpage/devops-driver
32
+ Project-URL: Documentation, https://github.com/marcpage/devops-driver
33
+ Project-URL: Repository, https://github.com/marcpage/devops-driver.git
34
+ Project-URL: Issues, https://github.com/marcpage/devops-driver/issues
35
+ Project-URL: Changelog, https://github.com/marcpage/devops-driver
36
+ Keywords: azure,devops,jira,confluence,email,pipelines,tools
37
+ Classifier: Development Status :: 1 - Planning
38
+ Classifier: Environment :: Console
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: Programming Language :: Python
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Topic :: Utilities
45
+ Classifier: Topic :: Software Development
46
+ Requires-Python: >=3.12
47
+ Description-Content-Type: text/markdown
48
+ License-File: LICENSE
49
+ Requires-Dist: PyYAML==6.0.1
50
+
51
+ ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
52
+ [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
53
+ [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
54
+ [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
55
+ [![GitHub top language](https://img.shields.io/github/languages/top/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
56
+ [![size sheild](https://img.shields.io/github/languages/code-size/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
57
+
58
+ [![example workflow](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml/badge.svg)](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
59
+ [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=99%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
60
+ [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
61
+ [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
62
+ [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
63
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
64
+
65
+ [![follow sheild](https://img.shields.io/github/followers/marcpage?label=Follow&style=social)](https://github.com/marcpage?tab=followers)
66
+ [![watch sheild](https://img.shields.io/github/watchers/marcpage/devops-driver?label=Watch&style=social)](https://github.com/marcpage/devops-driver/watchers)
67
+
68
+ # devops-driver
69
+
70
+ Devops-driver is a set of tools to help streamline developer's experience. It is a collection of tools to help gain insights into various processes.
@@ -0,0 +1,20 @@
1
+ ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
2
+ [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
3
+ [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
4
+ [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
5
+ [![GitHub top language](https://img.shields.io/github/languages/top/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
6
+ [![size sheild](https://img.shields.io/github/languages/code-size/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
7
+
8
+ [![example workflow](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml/badge.svg)](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
9
+ [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=99%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
10
+ [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
11
+ [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
12
+ [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
13
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
14
+
15
+ [![follow sheild](https://img.shields.io/github/followers/marcpage?label=Follow&style=social)](https://github.com/marcpage?tab=followers)
16
+ [![watch sheild](https://img.shields.io/github/watchers/marcpage/devops-driver?label=Watch&style=social)](https://github.com/marcpage/devops-driver/watchers)
17
+
18
+ # devops-driver
19
+
20
+ Devops-driver is a set of tools to help streamline developer's experience. It is a collection of tools to help gain insights into various processes.
@@ -0,0 +1,5 @@
1
+ """ DevOps tools """
2
+
3
+ __version__ = "0.1.32"
4
+ __author__ = "Marc Page"
5
+ __credits__ = ""
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """ Settings that can be in files, environment, or on the command line
5
+
6
+ Settings can be set on the command line, environment, in the code, or in files.
7
+
8
+ Priority order:
9
+ - in code
10
+ - command line
11
+ - environment
12
+ - files
13
+
14
+ Files can be JSON or YAML.
15
+ They must have .json, .yml, or .yaml extension.
16
+ Files can be named after the file passed in (__file__) or "devopsdriver".
17
+ All files named after the file passed in have priority over "devopsdriver".
18
+ This allows for shared settings for multiple scripts as well as specific settings.
19
+ Files can be in an OS-specific preferences location, a series of specified directories,
20
+ or next to the file passed in (__file__).
21
+ This allows for secrets, keys, and tokens to be stored on the machine and not in the repo.
22
+
23
+ The OS specific directories are:
24
+ - macOS: ~/Library/Preferences/
25
+ - Windows: %APPDATA%/
26
+ - Linux: ~/.devopsdriver/
27
+
28
+ You can have environment variable substitutions in the values in the files.
29
+ For instance, you can specify:
30
+
31
+ output: ${home}/reports
32
+
33
+ The `${home}` will be replaced with the value of the HOME environment variable, if it exists.
34
+ If the environment variable does not exist, no change is made.
35
+
36
+ Use case 1: Secrets not in repo
37
+ tokens, passwords, etc can be stored in <pref>/devopsdriver.yml
38
+ This will allow for all scripts to access those secrets
39
+ but they are not in the repo.
40
+ For pipeline runs these secrets can be passed on the command line or in the environment.
41
+
42
+ Use case 2: common settings among scripts
43
+ Store your common settings in devopsdriver.yml next to your script.
44
+ All scripts in this directory will have access to these settings.
45
+ For instance, say you want all emails sent from the same person.
46
+ You could set 'email: me@domain.com' in devopsdriver.html.
47
+ You could also, store report paths, emails, groups, holidays, etc.
48
+ Any data all scripts may want to have access to.
49
+
50
+ Use case 3: configurable settings
51
+ Any settings in your script that you may want to configure.
52
+ If your script is `cool_script.py` then put the settings in `cool_script.yml` next to it.
53
+ This could be colors, emails, repos, queries, whatever.
54
+
55
+ Use case 4: override secrets for specific script
56
+ Overriding secrets stored in <pref>/devopsdrive.yml for specific scripts.
57
+ If your script is `cool_script.py` save them to <pref>cool_script.yml.
58
+
59
+ *Note*: You can override a specific setting in a sub-dictionary.
60
+ For instance, say <pref>/devopsdrive.yml:
61
+
62
+ ```yaml
63
+ api:
64
+ user: johndoe
65
+ password: Setec Astronomy
66
+ ```
67
+
68
+ You could override this for `cool_script.py` be adding <pref>cool_script.yml:
69
+
70
+ ```yaml
71
+ api:
72
+ user: janedoe
73
+ ```
74
+
75
+ johndoe and janedoe share the same password, so you just need to update the `user`
76
+ """
77
+
78
+
79
+ from json import load
80
+ from os.path import dirname, basename, splitext, join
81
+ from os import environ as os_environ, makedirs as os_makedirs
82
+ from re import compile as regex
83
+ from platform import system as os_system
84
+ from sys import argv as sys_argv
85
+
86
+ from yaml import safe_load
87
+
88
+
89
+ # for testing
90
+ ENVIRON = os_environ
91
+ ARGV = sys_argv
92
+ SYSTEM = os_system
93
+ MAKEDIRS = os_makedirs
94
+ SHARED = "devopsdriver"
95
+ PRINT = print
96
+
97
+
98
+ def load_json(path: str) -> dict:
99
+ """Load a dictionary from a JSON file
100
+
101
+ Args:
102
+ path (str): Path to JSON file
103
+
104
+ Returns:
105
+ dict: The contents as a dictionary, or empty dictioanry if unable to load the file
106
+ """
107
+ try:
108
+ with open(path, "r", encoding="utf-8") as file:
109
+ return load(file)
110
+
111
+ except FileNotFoundError:
112
+ return {}
113
+
114
+
115
+ def load_yaml(path: str) -> dict:
116
+ """Load a dictionary from a JSON file
117
+
118
+ Args:
119
+ path (str): Path to JSON file
120
+
121
+ Returns:
122
+ dict: The contents as a dictionary, or empty dictioanry if unable to load the file
123
+ """
124
+ try:
125
+ with open(path, "r", encoding="utf-8") as file:
126
+ return safe_load(file)
127
+
128
+ except FileNotFoundError:
129
+ return {}
130
+
131
+
132
+ class Settings:
133
+ """Settings object"""
134
+
135
+ FORMATS = ((".yml", load_yaml), (".yaml", load_yaml), (".json", load_json))
136
+ DEFAULT_PREF_DIR = "Linux"
137
+ PREF_DIR = {
138
+ "Darwin": join(ENVIRON.get("HOME", ""), "Library", "Preferences"),
139
+ "Windows": join(ENVIRON.get("APPDATA", "")),
140
+ "Linux": join(ENVIRON.get("HOME", ""), ".devopsdriver"),
141
+ }
142
+ ENV_VAR_PATTERN = regex(r"\${(\S+)}")
143
+
144
+ def __init__(self, file: str, *directories, **settings):
145
+ """Create a settings object using a file, directories to search, and settings overrides
146
+
147
+ Args:
148
+ file (str): The basename to use and a directory to search. pass __file__
149
+ """
150
+ self.overrides = settings
151
+ directories = [dirname(file), *directories, Settings.__preferences_dir()]
152
+ search_info = Settings.__all_paths(file, directories)
153
+ self.search_files = [join(d, n + e) for e, n, d, _ in search_info]
154
+ self.settings = Settings.__find_all_settings(search_info)
155
+ self.opts = {}
156
+ self.environ = {}
157
+
158
+ def cli(self, key: str, name: str = None):
159
+ """Sets a command line switch to map to a settings value.
160
+
161
+ Args:
162
+ key (str): The settings key it maps to, dotted for inside dictionary
163
+ name (str): Name of the command line switch, eg '-p' or '--path'
164
+ If name is not specified, the key is a settings value
165
+ to lookup up the mappings for keys to switches
166
+
167
+ Returns:
168
+ Settings: Returns self so you can chain calls
169
+ """
170
+ if name is None:
171
+ for setting_key, env_name in self.settings.get(key, {}).items():
172
+ self.opts[setting_key] = env_name
173
+ return self
174
+
175
+ self.opts[key] = name
176
+ return self
177
+
178
+ def env(self, key: str, name: str = None):
179
+ """Sets an environment variable to map to a settings value.
180
+
181
+ Args:
182
+ key (str): The settings key it maps to, dotted for inside dictionary
183
+ name (str): Name of the environment variable
184
+ If name is not specified, the key is a settings value
185
+ to lookup up the mappings for keys to environment names
186
+
187
+ Returns:
188
+ Settings: Returns self so you can chain calls
189
+ """
190
+ if name is None:
191
+ for setting_key, cli_name in self.settings.get(key, {}).items():
192
+ self.environ[setting_key] = cli_name
193
+ return self
194
+
195
+ self.environ[key] = name
196
+ return self
197
+
198
+ @staticmethod
199
+ def __patch_instance(key: str) -> str:
200
+ for env_key, value in ENVIRON.items():
201
+ if env_key.lower() == key.lower():
202
+ return value
203
+
204
+ return "${" + key + "}"
205
+
206
+ @staticmethod
207
+ def __patch(value: any) -> any:
208
+ if isinstance(value, str):
209
+ return Settings.ENV_VAR_PATTERN.sub(
210
+ lambda m: Settings.__patch_instance(m.group(1)), value
211
+ )
212
+
213
+ return value
214
+
215
+ def __lookup(self, key: str, check: bool, default: any = None) -> any:
216
+ # Settings passed in override everything
217
+ if key in self.overrides:
218
+ return True if check else self.overrides[key]
219
+
220
+ # Settings on the command line take next precedence
221
+ if key in self.opts:
222
+ for nth, name in enumerate(ARGV[1:]):
223
+ if name.lower() == self.opts[key].lower() and nth + 2 < len(ARGV):
224
+ return True if check else ARGV[nth + 2]
225
+
226
+ # Settings in the environment are next
227
+ if key in self.environ:
228
+ for e_key in ENVIRON:
229
+ if e_key.lower() == self.environ[key].lower():
230
+ return True if check else ENVIRON[e_key]
231
+
232
+ # Last check the files for settings
233
+ keys = key.split(".")
234
+ level = self.settings
235
+
236
+ for key_part in keys[:-1]:
237
+ level = level.get(key_part, {})
238
+
239
+ if check:
240
+ return keys[-1] in level
241
+
242
+ return Settings.__patch(level.get(keys[-1], default))
243
+
244
+ def get(self, key: str, default: any = None) -> any:
245
+ """Dictionary-like get
246
+
247
+ Args:
248
+ key (str): The key to load, dotted format
249
+ default (any, optional): The value to return if there is no value. Defaults to None.
250
+
251
+ Returns:
252
+ any: The value or `default` if not found
253
+ """
254
+ return self.__lookup(key, check=False, default=default)
255
+
256
+ def has(self, key: str) -> bool:
257
+ """Check if the key exists in any of the environments
258
+
259
+ Args:
260
+ key (str): The dotted key
261
+
262
+ Returns:
263
+ bool: True if the key exists
264
+ """
265
+ return self.__lookup(key, check=True)
266
+
267
+ def __getitem__(self, key: str) -> any:
268
+ if not self.has(key):
269
+ raise KeyError(key)
270
+
271
+ return self.get(key)
272
+
273
+ @staticmethod
274
+ def __preferences_dir() -> str:
275
+ default_dir = Settings.PREF_DIR[Settings.DEFAULT_PREF_DIR]
276
+ directory = Settings.PREF_DIR.get(SYSTEM(), default_dir)
277
+ MAKEDIRS(directory, exist_ok=True)
278
+ return directory
279
+
280
+ @staticmethod
281
+ def __merge(base: dict, new: dict):
282
+ """Add new information to an existing dictionary, if it doesn't already exist.
283
+ If new has keys that base doesn't, they are added.
284
+ If new has a key that base does and they both are dictionaries, they are merged.
285
+
286
+ Args:
287
+ base (dict): The is the existing values to add to
288
+ new (dict): The new values to possible add
289
+ """
290
+ for key in new:
291
+ if key not in base:
292
+ base[key] = new[key]
293
+ continue
294
+
295
+ if isinstance(base[key], dict) and isinstance(new[key], dict):
296
+ Settings.__merge(base[key], new[key])
297
+
298
+ @staticmethod
299
+ def __all_paths(file: str, directories: list[str]) -> list[tuple]:
300
+ names = [splitext(basename(file))[0], SHARED]
301
+ return [
302
+ (e, n, d, f)
303
+ for n in names
304
+ for d in directories
305
+ for e, f in Settings.FORMATS
306
+ ]
307
+
308
+ @staticmethod
309
+ def __find_all_settings(search_info: list[tuple]) -> dict:
310
+ settings = {}
311
+
312
+ for extension, name, directory, loader in search_info:
313
+ contents = loader(join(directory, name + extension))
314
+ Settings.__merge(settings, contents)
315
+
316
+ return settings
317
+
318
+
319
+ def main() -> None:
320
+ """Get settings values"""
321
+ settings = Settings(__file__, dirname(dirname(__file__)))
322
+
323
+ for arg in ARGV[1:]:
324
+ PRINT(settings.get(arg))
325
+
326
+
327
+ if __name__ == "__main__":
328
+ main()
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.1
2
+ Name: devopsdriver
3
+ Version: 0.1.32
4
+ Summary: DevOps tools
5
+ Author-email: Marc Page <marcallenpage@gmail.com>
6
+ License: This is free and unencumbered software released into the public domain.
7
+
8
+ Anyone is free to copy, modify, publish, use, compile, sell, or
9
+ distribute this software, either in source code form or as a compiled
10
+ binary, for any purpose, commercial or non-commercial, and by any
11
+ means.
12
+
13
+ In jurisdictions that recognize copyright laws, the author or authors
14
+ of this software dedicate any and all copyright interest in the
15
+ software to the public domain. We make this dedication for the benefit
16
+ of the public at large and to the detriment of our heirs and
17
+ successors. We intend this dedication to be an overt act of
18
+ relinquishment in perpetuity of all present and future rights to this
19
+ software under copyright law.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ For more information, please refer to <https://unlicense.org>
30
+
31
+ Project-URL: Homepage, https://github.com/marcpage/devops-driver
32
+ Project-URL: Documentation, https://github.com/marcpage/devops-driver
33
+ Project-URL: Repository, https://github.com/marcpage/devops-driver.git
34
+ Project-URL: Issues, https://github.com/marcpage/devops-driver/issues
35
+ Project-URL: Changelog, https://github.com/marcpage/devops-driver
36
+ Keywords: azure,devops,jira,confluence,email,pipelines,tools
37
+ Classifier: Development Status :: 1 - Planning
38
+ Classifier: Environment :: Console
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: Programming Language :: Python
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Topic :: Utilities
45
+ Classifier: Topic :: Software Development
46
+ Requires-Python: >=3.12
47
+ Description-Content-Type: text/markdown
48
+ License-File: LICENSE
49
+ Requires-Dist: PyYAML==6.0.1
50
+
51
+ ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
52
+ [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
53
+ [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
54
+ [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
55
+ [![GitHub top language](https://img.shields.io/github/languages/top/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
56
+ [![size sheild](https://img.shields.io/github/languages/code-size/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
57
+
58
+ [![example workflow](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml/badge.svg)](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
59
+ [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=99%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
60
+ [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
61
+ [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
62
+ [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
63
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
64
+
65
+ [![follow sheild](https://img.shields.io/github/followers/marcpage?label=Follow&style=social)](https://github.com/marcpage?tab=followers)
66
+ [![watch sheild](https://img.shields.io/github/watchers/marcpage/devops-driver?label=Watch&style=social)](https://github.com/marcpage/devops-driver/watchers)
67
+
68
+ # devops-driver
69
+
70
+ Devops-driver is a set of tools to help streamline developer's experience. It is a collection of tools to help gain insights into various processes.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ devopsdriver/__init__.py
5
+ devopsdriver/settings.py
6
+ devopsdriver.egg-info/PKG-INFO
7
+ devopsdriver.egg-info/SOURCES.txt
8
+ devopsdriver.egg-info/dependency_links.txt
9
+ devopsdriver.egg-info/requires.txt
10
+ devopsdriver.egg-info/top_level.txt
11
+ tests/test_settings.py
@@ -0,0 +1 @@
1
+ PyYAML==6.0.1
@@ -0,0 +1 @@
1
+ devopsdriver
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "devopsdriver"
3
+ description = "DevOps tools"
4
+ readme = "README.md"
5
+ license = {file = "LICENSE"}
6
+ dynamic = ["version"]
7
+ requires-python = ">= 3.12"
8
+ dependencies = [
9
+ "PyYAML==6.0.1",
10
+ ]
11
+ keywords = ["azure", "devops", "jira", "confluence", "email", "pipelines", "tools"]
12
+ classifiers=[
13
+ "Development Status :: 1 - Planning",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: The Unlicense (Unlicense)",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Utilities",
21
+ "Topic :: Software Development",
22
+ ]
23
+ authors = [
24
+ {name = "Marc Page", email = "marcallenpage@gmail.com"},
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/marcpage/devops-driver"
29
+ Documentation = "https://github.com/marcpage/devops-driver"
30
+ Repository = "https://github.com/marcpage/devops-driver.git"
31
+ Issues = "https://github.com/marcpage/devops-driver/issues"
32
+ Changelog = "https://github.com/marcpage/devops-driver"
33
+
34
+ [tool.setuptools.dynamic]
35
+ version = {attr = "devopsdriver.__version__"}
36
+
37
+ [build-system]
38
+ requires = ["setuptools >= 69.2"]
39
+ build-backend = "setuptools.build_meta"
40
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Tests Settings class """
4
+
5
+ from tempfile import TemporaryDirectory
6
+ from os.path import join, splitext, dirname
7
+ from os import makedirs
8
+ from json import dump
9
+ from string import ascii_lowercase
10
+ from itertools import product
11
+
12
+ from yaml import safe_dump
13
+
14
+ import devopsdriver.settings as settings
15
+
16
+
17
+ def __setup_settings(os: str = "Linux", shared: str = "test", **pref_dirs) -> None:
18
+ settings.ENVIRON = {}
19
+ settings.ARGV = []
20
+ settings.SYSTEM = lambda: os
21
+ settings.SHARED = shared
22
+ settings.PRINT = lambda s: s
23
+ # settings.MAKEDIRS = lambda p: p
24
+ # settings.Settings.FORMATS = None
25
+ settings.Settings.PREF_DIR = pref_dirs
26
+
27
+
28
+ def ensure(directory: str) -> str:
29
+ """Ensures that a directory exists before using
30
+
31
+ Args:
32
+ directory (str): The directory to create
33
+
34
+ Returns:
35
+ str: The directory that now exists
36
+ """
37
+ makedirs(directory, exist_ok=True)
38
+ return directory
39
+
40
+
41
+ def __write(path: str, **options) -> None:
42
+ ensure(dirname(path))
43
+
44
+ with open(path, "w", encoding="utf-8") as settings_file:
45
+ if splitext(path)[1] == ".json":
46
+ dump(options, settings_file)
47
+ else:
48
+ safe_dump(options, settings_file)
49
+
50
+
51
+ def __setup_files(directory: str, dir1: str, dir2: str) -> None:
52
+ """
53
+ Priorities:
54
+ <dir>/main.yml a
55
+ <dir>/main.yaml b
56
+ <dir>/main.json c
57
+ <dir1>/main.yml d
58
+ <dir1>/main.yaml e
59
+ <dir1>/main.json f
60
+ <dir2>/main.yml g
61
+ <dir2>/main.yaml h
62
+ <dir2>/main.json i
63
+ <pref>/main.yml j
64
+ <pref>/main.yaml k
65
+ <pref>/main.json l
66
+ <dir>/test.yml m
67
+ <dir>/test.yaml n
68
+ <dir>/test.json o
69
+ <dir1>/test.yml p
70
+ <dir1>/test.yaml q
71
+ <dir1>/test.json r
72
+ <dir2>/test.yml s
73
+ <dir2>/test.yaml t
74
+ <dir2>/test.json u
75
+ <pref>/test.yml v
76
+ <pref>/test.yaml w
77
+ <pref>/test.json x
78
+ """
79
+ lin_dir = ensure(settings.Settings.PREF_DIR["Linux"])
80
+ mac_dir = ensure(settings.Settings.PREF_DIR["Darwin"])
81
+ win_dir = ensure(settings.Settings.PREF_DIR["Windows"])
82
+ letters = list(ascii_lowercase)
83
+
84
+ for name in ("test", "main"):
85
+ for letter, os_dir in (("l", lin_dir), ("w", win_dir), ("m", mac_dir)):
86
+ __write(
87
+ join(os_dir, "test.json"),
88
+ **{l: f"{name[0]}{letter}{l}" for l in letters},
89
+ dp={l: f"{name[0]}{letter}{l}" for l in letters},
90
+ )
91
+
92
+ letters.pop()
93
+
94
+ for directory, name, ext in product(
95
+ (dir2, dir1, directory), ("test", "main"), (".json", ".yaml", ".yml")
96
+ ):
97
+ __write(
98
+ join(directory, name + ext),
99
+ **{l: f"{directory[-1]}{ext[2]}{name[0]}{letter}{l}" for l in letters},
100
+ dp={l: f"{directory[-1]}{ext[2]}{name[0]}{letter}{l}" for l in letters},
101
+ )
102
+ letters.pop()
103
+
104
+
105
+ def test_basic():
106
+ """test the basic functionality"""
107
+ with TemporaryDirectory() as working_dir:
108
+ base_dir = join(working_dir, "base")
109
+
110
+ for os in ("Linux", "Darwin", "Windows", "Unknown"):
111
+ __setup_settings(
112
+ os=os,
113
+ shared="test",
114
+ Linux=join(base_dir, "Linux"),
115
+ Darwin=join(base_dir, "macOS"),
116
+ Windows=join(base_dir, "Windows"),
117
+ )
118
+ dir1 = join(base_dir, "dir1")
119
+ dir2 = join(base_dir, "dir2")
120
+
121
+ __setup_files(base_dir, dir1, dir2)
122
+ settings.ENVIRON = {
123
+ "alpha": "aaenviron",
124
+ "yota": "yyenviron",
125
+ "zeta": "zzenviron",
126
+ }
127
+ settings.ARGV = ["exe", "--beta", "bbcli", "--zeta", "zzcli"]
128
+ opts = (
129
+ settings.Settings(
130
+ join(base_dir, "main.py"), dir1, dir2, aa=1, bb=2, cc=3
131
+ )
132
+ .cli("bb", "--beta")
133
+ .cli("zz", "--zeta")
134
+ .env("aa", "alpha")
135
+ .env("yy", "yota")
136
+ )
137
+ ltr = {"Linux": "l", "Darwin": "m", "Windows": "w", "Unknown": "l"}
138
+ assert opts["yy"] == "yyenviron", opts["yy"]
139
+ assert opts["aa"] == 1, f"{opts['aa']} {os}"
140
+ assert opts["bb"] == 2, f"{opts['bb']} {os}"
141
+ assert opts["cc"] == 3, f"{opts['cc']} {os}"
142
+ assert opts["zz"] == "zzcli", opts["zz"]
143
+ assert opts["a"] == "emmma", f"{opts['a']} {os}"
144
+ assert opts["dp.a"] == "emmma", f"{opts['dp.a']} {os}"
145
+ assert opts["b"] == "emmmb", f"{opts['b']} {os}"
146
+ assert opts["dp.b"] == "emmmb", f"{opts['dp.b']} {os}"
147
+ assert opts["c"] == "emmmc", f"{opts['c']} {os}"
148
+ assert opts["dp.c"] == "emmmc", f"{opts['dp.c']} {os}"
149
+ assert opts["d"] == "emmmd", f"{opts['d']} {os}"
150
+ assert opts["dp.d"] == "emmmd", f"{opts['dp.d']} {os}"
151
+ assert opts["e"] == "emmme", f"{opts['e']} {os}"
152
+ assert opts["dp.e"] == "emmme", f"{opts['dp.e']} {os}"
153
+ assert opts["f"] == "emmmf", f"{opts['f']} {os}"
154
+ assert opts["dp.f"] == "emmmf", f"{opts['dp.f']} {os}"
155
+ assert opts["g"] == "emmmg", f"{opts['g']} {os}"
156
+ assert opts["dp.g"] == "emmmg", f"{opts['dp.g']} {os}"
157
+ assert opts["h"] == "eammh", f"{opts['h']} {os}"
158
+ assert opts["dp.h"] == "eammh", f"{opts['dp.h']} {os}"
159
+ assert opts["i"] == "esmmi", f"{opts['i']} {os}"
160
+ assert opts["dp.i"] == "esmmi", f"{opts['dp.i']} {os}"
161
+ assert opts["j"] == "1mmmj", f"{opts['j']} {os}"
162
+ assert opts["dp.j"] == "1mmmj", f"{opts['dp.j']} {os}"
163
+ assert opts["k"] == "1mmmk", f"{opts['k']} {os}"
164
+ assert opts["dp.k"] == "1mmmk", f"{opts['dp.k']} {os}"
165
+ assert opts["l"] == "1mmml", f"{opts['l']} {os}"
166
+ assert opts["dp.l"] == "1mmml", f"{opts['dp.l']} {os}"
167
+ assert opts["m"] == "1mmmm", f"{opts['m']} {os}"
168
+ assert opts["dp.m"] == "1mmmm", f"{opts['dp.m']} {os}"
169
+ assert opts["n"] == "1ammn", f"{opts['n']} {os}"
170
+ assert opts["dp.n"] == "1ammn", f"{opts['dp.n']} {os}"
171
+ assert opts["o"] == "1smmo", f"{opts['o']} {os}"
172
+ assert opts["dp.o"] == "1smmo", f"{opts['dp.o']} {os}"
173
+ assert opts["p"] == "2mmmp", f"{opts['p']} {os}"
174
+ assert opts["dp.p"] == "2mmmp", f"{opts['dp.p']} {os}"
175
+ assert opts["q"] == "2mmmq", f"{opts['q']} {os}"
176
+ assert opts["dp.q"] == "2mmmq", f"{opts['dp.q']} {os}"
177
+ assert opts["r"] == "2mmmr", f"{opts['r']} {os}"
178
+ assert opts["dp.r"] == "2mmmr", f"{opts['dp.r']} {os}"
179
+ assert opts["s"] == "2mmms", f"{opts['s']} {os}"
180
+ assert opts["dp.s"] == "2mmms", f"{opts['dp.s']} {os}"
181
+ assert opts["t"] == "2ammt", f"{opts['t']} {os}"
182
+ assert opts["dp.t"] == "2ammt", f"{opts['dp.t']} {os}"
183
+ assert opts["u"] == "2smmu", f"{opts['u']} {os}"
184
+ assert opts["dp.u"] == "2smmu", f"{opts['dp.u']} {os}"
185
+ assert opts["v"] == "2mtmv", f"{opts['v']} {os}"
186
+ assert opts["dp.v"] == "2mtmv", f"{opts['dp.v']} {os}"
187
+ assert opts["w"] == "2atmw", f"{opts['w']} {os}"
188
+ assert opts["dp.w"] == "2atmw", f"{opts['dp.w']} {os}"
189
+ assert opts["x"] == "2stmx", f"{opts['x']} {os}"
190
+ assert opts["dp.x"] == "2stmx", f"{opts['dp.x']} {os}"
191
+ assert opts["y"] == f"m{ltr[os]}y", f"{opts['y']} m{ltr[os]}y {os}"
192
+ assert opts["dp.y"] == f"m{ltr[os]}y", f"{opts['dp.y']} m{ltr[os]}y {os}"
193
+
194
+ try:
195
+ assert opts["yoodle"] is not None
196
+ assert True, "Should have thrown exception"
197
+
198
+ except KeyError:
199
+ pass
200
+
201
+
202
+ def test_cli_env_in_yaml():
203
+ """test setting cli and env lookups in the yaml itself"""
204
+ with TemporaryDirectory() as working_dir:
205
+ base_dir = join(working_dir, "base")
206
+ __setup_settings(
207
+ os="Linux",
208
+ shared="test",
209
+ Linux=join(base_dir, "Linux"),
210
+ Darwin=join(base_dir, "macOS"),
211
+ Windows=join(base_dir, "Windows"),
212
+ )
213
+ __write(
214
+ join(base_dir, "main.yml"),
215
+ env={"aa": "alpha", "yy": "yota"},
216
+ cli={"bb": "--beta"},
217
+ yy="main yy",
218
+ aa="main aa",
219
+ bb="main bb",
220
+ )
221
+ __write(
222
+ join(base_dir, "test.yml"),
223
+ env={"zz": "zeta"},
224
+ cli={"dd": "--delta"},
225
+ zz="test zz",
226
+ dd="test dd",
227
+ )
228
+ settings.ENVIRON = {
229
+ "alpha": "environ aa",
230
+ "yota": "environ yy",
231
+ "zeta": "environ zz",
232
+ "delta": "environ dd",
233
+ }
234
+ settings.ARGV = [
235
+ "exe",
236
+ "--beta",
237
+ "cli bb",
238
+ "--zeta",
239
+ "cli zz",
240
+ "--delta",
241
+ "cli dd",
242
+ ]
243
+ opts = (
244
+ settings.Settings(join(base_dir, "main.py"), aa="code aa")
245
+ .cli("cli")
246
+ .env("env")
247
+ )
248
+ assert opts["aa"] == "code aa", opts["aa"]
249
+ assert opts["bb"] == "cli bb", opts["bb"]
250
+ assert opts["dd"] == "cli dd", opts["dd"]
251
+ assert opts["yy"] == "environ yy", opts["yy"]
252
+ assert opts["zz"] == "environ zz", opts["zz"]
253
+
254
+
255
+ def test_environ_values():
256
+ """test environment variable substitution"""
257
+ with TemporaryDirectory() as working_dir:
258
+ base_dir = join(working_dir, "base")
259
+ __setup_settings(
260
+ os="Linux",
261
+ shared="test",
262
+ Linux=join(base_dir, "Linux"),
263
+ Darwin=join(base_dir, "macOS"),
264
+ Windows=join(base_dir, "Windows"),
265
+ )
266
+ __write(
267
+ join(base_dir, "main.yml"),
268
+ output="${home}/reports",
269
+ settings="${appDir}/settings.json",
270
+ value="testing ${noenv} for noenv",
271
+ )
272
+ settings.ENVIRON = {
273
+ "HOME": "sweet home",
274
+ "APPDIR": "app data dir",
275
+ }
276
+ opts = (
277
+ settings.Settings(join(base_dir, "main.py"), aa="code aa")
278
+ .cli("cli")
279
+ .env("env")
280
+ )
281
+ assert opts["output"] == "sweet home/reports", opts["output"]
282
+ assert opts["settings"] == "app data dir/settings.json", opts["settings"]
283
+ assert opts["value"] == "testing ${noenv} for noenv", opts["value"]
284
+
285
+
286
+ def test_main():
287
+ """test the main entry point"""
288
+ with TemporaryDirectory() as working_dir:
289
+ __setup_settings(shared="test", Linux=join(working_dir, "Linux"))
290
+ settings.ARGV = ["ignore", "test"]
291
+ __write(join(working_dir, "Linux", "test.yml"), test=3)
292
+ settings.main()
293
+
294
+
295
+ if __name__ == "__main__":
296
+ test_main()
297
+ test_environ_values()
298
+ test_cli_env_in_yaml()
299
+ test_basic()