devopsdriver 0.1.32__tar.gz → 0.1.34__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devopsdriver
3
- Version: 0.1.32
3
+ Version: 0.1.34
4
4
  Summary: DevOps tools
5
5
  Author-email: Marc Page <marcallenpage@gmail.com>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -43,12 +43,14 @@ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
43
43
  Classifier: Programming Language :: Python :: 3.12
44
44
  Classifier: Topic :: Utilities
45
45
  Classifier: Topic :: Software Development
46
- Requires-Python: >=3.12
46
+ Requires-Python: >=3.10
47
47
  Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: PyYAML==6.0.1
50
+ Requires-Dist: keyring==25.0.0
50
51
 
51
52
  ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
53
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.34&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.34/)
52
54
  [![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
55
  [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
54
56
  [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
@@ -1,4 +1,5 @@
1
1
  ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
2
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.34&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.34/)
2
3
  [![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
4
  [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
4
5
  [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
@@ -1,5 +1,5 @@
1
1
  """ DevOps tools """
2
2
 
3
- __version__ = "0.1.32"
3
+ __version__ = "0.1.34"
4
4
  __author__ = "Marc Page"
5
5
  __credits__ = ""
@@ -82,9 +82,10 @@ from os import environ as os_environ, makedirs as os_makedirs
82
82
  from re import compile as regex
83
83
  from platform import system as os_system
84
84
  from sys import argv as sys_argv
85
+ from getpass import getpass as os_getpass
85
86
 
86
87
  from yaml import safe_load
87
-
88
+ from keyring import get_password, set_password
88
89
 
89
90
  # for testing
90
91
  ENVIRON = os_environ
@@ -93,6 +94,9 @@ SYSTEM = os_system
93
94
  MAKEDIRS = os_makedirs
94
95
  SHARED = "devopsdriver"
95
96
  PRINT = print
97
+ GET_PASSWORD = get_password
98
+ SET_PASSWORD = set_password
99
+ GET_PASS = os_getpass
96
100
 
97
101
 
98
102
  def load_json(path: str) -> dict:
@@ -154,6 +158,30 @@ class Settings:
154
158
  self.settings = Settings.__find_all_settings(search_info)
155
159
  self.opts = {}
156
160
  self.environ = {}
161
+ self.secrets = {}
162
+
163
+ def __bypass(self, key: str, name: str, store: dict):
164
+ if name is None:
165
+ for setting_key, store_name in self.settings.get(key, {}).items():
166
+ store[setting_key] = store_name
167
+ return self
168
+
169
+ store[key] = name
170
+ return self
171
+
172
+ def key(self, key: str, name: str = None):
173
+ """Sets a keychain name to map to a settings value.
174
+
175
+ Args:
176
+ key (str): The settings key it maps to, dotted for inside dictionary
177
+ name (str): Name of the keychain key
178
+ If name is not specified, the key is a settings value
179
+ to lookup up the mappings for keys to switches
180
+
181
+ Returns:
182
+ Settings: Returns self so you can chain calls
183
+ """
184
+ return self.__bypass(key, name, self.secrets)
157
185
 
158
186
  def cli(self, key: str, name: str = None):
159
187
  """Sets a command line switch to map to a settings value.
@@ -167,13 +195,7 @@ class Settings:
167
195
  Returns:
168
196
  Settings: Returns self so you can chain calls
169
197
  """
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
198
+ return self.__bypass(key, name, self.opts)
177
199
 
178
200
  def env(self, key: str, name: str = None):
179
201
  """Sets an environment variable to map to a settings value.
@@ -187,13 +209,7 @@ class Settings:
187
209
  Returns:
188
210
  Settings: Returns self so you can chain calls
189
211
  """
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
212
+ return self.__bypass(key, name, self.environ)
197
213
 
198
214
  @staticmethod
199
215
  def __patch_instance(key: str) -> str:
@@ -212,6 +228,23 @@ class Settings:
212
228
 
213
229
  return value
214
230
 
231
+ @staticmethod
232
+ def split_key(key: str) -> tuple[str, str]:
233
+ """Splits a keychain name into service and name
234
+
235
+ Args:
236
+ key (str): If there is a / then it is service/name
237
+ otherwise it is "system"/name
238
+
239
+ Returns:
240
+ tuple[str, str]: The service and name
241
+ """
242
+ parts = key.split("/", 1)
243
+ assert len(parts) in {1, 2}, parts
244
+ service = parts[0] if len(parts) == 2 else "system"
245
+ secret_name = parts[1] if len(parts) == 2 else parts[0]
246
+ return (service, secret_name)
247
+
215
248
  def __lookup(self, key: str, check: bool, default: any = None) -> any:
216
249
  # Settings passed in override everything
217
250
  if key in self.overrides:
@@ -229,6 +262,13 @@ class Settings:
229
262
  if e_key.lower() == self.environ[key].lower():
230
263
  return True if check else ENVIRON[e_key]
231
264
 
265
+ # Settings in the keychain are next
266
+ if key in self.secrets:
267
+ value = GET_PASSWORD(*Settings.split_key(self.secrets[key]))
268
+
269
+ if value is not None:
270
+ return True if check else value
271
+
232
272
  # Last check the files for settings
233
273
  keys = key.split(".")
234
274
  level = self.settings
@@ -318,9 +358,21 @@ class Settings:
318
358
 
319
359
  def main() -> None:
320
360
  """Get settings values"""
321
- settings = Settings(__file__, dirname(dirname(__file__)))
361
+ settings = Settings(__file__, dirname(dirname(__file__))).key("secrets")
362
+
363
+ args = list(ARGV[1:])
364
+
365
+ if "--set_secrets" in args:
366
+ args.remove("--set_secrets")
367
+
368
+ for secret, key in settings.secrets.items():
369
+ if not settings.has(secret):
370
+ value = GET_PASS(f"{secret} ({key}): ")
371
+
372
+ if value:
373
+ SET_PASSWORD(*Settings.split_key(key), value)
322
374
 
323
- for arg in ARGV[1:]:
375
+ for arg in args:
324
376
  PRINT(settings.get(arg))
325
377
 
326
378
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devopsdriver
3
- Version: 0.1.32
3
+ Version: 0.1.34
4
4
  Summary: DevOps tools
5
5
  Author-email: Marc Page <marcallenpage@gmail.com>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -43,12 +43,14 @@ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
43
43
  Classifier: Programming Language :: Python :: 3.12
44
44
  Classifier: Topic :: Utilities
45
45
  Classifier: Topic :: Software Development
46
- Requires-Python: >=3.12
46
+ Requires-Python: >=3.10
47
47
  Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: PyYAML==6.0.1
50
+ Requires-Dist: keyring==25.0.0
50
51
 
51
52
  ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
53
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.34&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.34/)
52
54
  [![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
55
  [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
54
56
  [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
@@ -0,0 +1,2 @@
1
+ PyYAML==6.0.1
2
+ keyring==25.0.0
@@ -4,9 +4,10 @@ description = "DevOps tools"
4
4
  readme = "README.md"
5
5
  license = {file = "LICENSE"}
6
6
  dynamic = ["version"]
7
- requires-python = ">= 3.12"
7
+ requires-python = ">= 3.10"
8
8
  dependencies = [
9
9
  "PyYAML==6.0.1",
10
+ "keyring==25.0.0",
10
11
  ]
11
12
  keywords = ["azure", "devops", "jira", "confluence", "email", "pipelines", "tools"]
12
13
  classifiers=[
@@ -20,6 +20,9 @@ def __setup_settings(os: str = "Linux", shared: str = "test", **pref_dirs) -> No
20
20
  settings.SYSTEM = lambda: os
21
21
  settings.SHARED = shared
22
22
  settings.PRINT = lambda s: s
23
+ settings.GET_PASSWORD = lambda s, n: f"{s}:{n}"
24
+ settings.GET_PASS = lambda p: p
25
+ settings.SET_PASSWORD = lambda s, n, p: f"{s} {n} {p}"
23
26
  # settings.MAKEDIRS = lambda p: p
24
27
  # settings.Settings.FORMATS = None
25
28
  settings.Settings.PREF_DIR = pref_dirs
@@ -292,7 +295,48 @@ def test_main():
292
295
  settings.main()
293
296
 
294
297
 
298
+ def test_secret():
299
+ """test os secret storage"""
300
+ with TemporaryDirectory() as working_dir:
301
+ base_dir = join(working_dir, "base")
302
+ passwords = {"system": {"john": "setec astronomy"}}
303
+ __setup_settings(
304
+ os="Linux",
305
+ shared="test",
306
+ Linux=join(base_dir, "Linux"),
307
+ Darwin=join(base_dir, "macOS"),
308
+ Windows=join(base_dir, "Windows"),
309
+ )
310
+ settings.GET_PASSWORD = lambda s, e: passwords.get(s, {}).get(e, None)
311
+ __write(join(base_dir, "main.yml"), password="main")
312
+ opts = settings.Settings(join(base_dir, "main.py")).key(
313
+ "password", "system/john"
314
+ )
315
+ assert opts["password"] == "setec astronomy", opts["password"]
316
+
317
+
318
+ def test_main_set_secret():
319
+ """test the main entry point when settings keychain secrets"""
320
+
321
+ def set_password(s, n, p):
322
+ assert s == "azure" and n == "token" and p == "setec astronomy", f"{s} {n} {p}"
323
+
324
+ with TemporaryDirectory() as working_dir:
325
+ __setup_settings(shared="test", Linux=join(working_dir, "Linux"))
326
+ settings.ARGV = ["ignore", "--set_secrets"]
327
+ settings.GET_PASSWORD = lambda s, n: None
328
+ settings.GET_PASS = lambda p: "setec astronomy"
329
+ settings.SET_PASSWORD = set_password
330
+ __write(
331
+ join(working_dir, "Linux", "test.yml"),
332
+ secrets={"azure.token": "azure/token"},
333
+ )
334
+ settings.main()
335
+
336
+
295
337
  if __name__ == "__main__":
338
+ test_main_set_secret()
339
+ test_secret()
296
340
  test_main()
297
341
  test_environ_values()
298
342
  test_cli_env_in_yaml()
@@ -1 +0,0 @@
1
- PyYAML==6.0.1
File without changes
File without changes