dtPyAppFramework 1.5.2__tar.gz → 2.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 (46) hide show
  1. {dtpyappframework-1.5.2/src/dtPyAppFramework.egg-info → dtpyappframework-2.0}/PKG-INFO +3 -3
  2. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/requirements.txt +3 -3
  3. dtpyappframework-2.0/src/dtPyAppFramework/_version.txt +1 -0
  4. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/__init__.py +1 -1
  5. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/__init__.py +9 -0
  6. dtpyappframework-2.0/src/dtPyAppFramework/settings/secrets/keystore.py +196 -0
  7. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/local_secret_store.py +22 -30
  8. dtpyappframework-2.0/src/dtPyAppFramework/settings/settings_reader.py +155 -0
  9. {dtpyappframework-1.5.2 → dtpyappframework-2.0/src/dtPyAppFramework.egg-info}/PKG-INFO +3 -3
  10. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/SOURCES.txt +4 -1
  11. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/requires.txt +2 -2
  12. dtpyappframework-2.0/tests/test_keystore.py +70 -0
  13. dtpyappframework-2.0/tests/test_settings_reader.py +54 -0
  14. dtpyappframework-1.5.2/src/dtPyAppFramework/_version.txt +0 -1
  15. dtpyappframework-1.5.2/src/dtPyAppFramework/settings/settings_reader.py +0 -88
  16. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/LICENCE.txt +0 -0
  17. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/MANIFEST.in +0 -0
  18. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/README.md +0 -0
  19. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/pyproject.toml +0 -0
  20. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/setup.cfg +0 -0
  21. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/setup.py +0 -0
  22. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/__init__.py +0 -0
  23. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_description.txt +0 -0
  24. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_licence.txt +0 -0
  25. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_metadata.yaml +0 -0
  26. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/_name.txt +0 -0
  27. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/application.py +0 -0
  28. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/__init__.py +0 -0
  29. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/aws.py +0 -0
  30. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/azure.py +0 -0
  31. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/cloud/cloud_session.py +0 -0
  32. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/decorators/__init__.py +0 -0
  33. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/logging/__init__.py +0 -0
  34. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/logging/default_logging.py +0 -0
  35. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/__init__.py +0 -0
  36. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/packaging/__init__.py +0 -0
  37. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/misc/yaml/__init__.py +0 -0
  38. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/paths/__init__.py +0 -0
  39. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/process/__init__.py +0 -0
  40. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/process/multiprocessing.py +0 -0
  41. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/resources/__init__.py +0 -0
  42. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/aws_secret_store.py +0 -0
  43. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/azure_secret_store.py +0 -0
  44. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework/settings/secrets/secret_store.py +0 -0
  45. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/dependency_links.txt +0 -0
  46. {dtpyappframework-1.5.2 → dtpyappframework-2.0}/src/dtPyAppFramework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dtPyAppFramework
3
- Version: 1.5.2
3
+ Version: 2.0
4
4
  Summary: A Python library for common features in application development.
5
5
  Author-email: Digital-Thought <dev@digital-thought.org>
6
6
  Maintainer-email: Digital-Thought <dev@digital-thought.org>
@@ -54,13 +54,13 @@ Requires-Dist: colorlog
54
54
  Requires-Dist: psutil
55
55
  Requires-Dist: pybase64
56
56
  Requires-Dist: boto3
57
- Requires-Dist: cryptography==39.0.2
57
+ Requires-Dist: cryptography
58
58
  Requires-Dist: azure-identity
59
59
  Requires-Dist: azure-keyvault-secrets
60
- Requires-Dist: pykeystore
61
60
  Requires-Dist: pytest-mock
62
61
  Requires-Dist: pytest-watch
63
62
  Requires-Dist: pytest
63
+ Requires-Dist: watchdog
64
64
 
65
65
  # dtPyAppFramework
66
66
 
@@ -3,10 +3,10 @@ colorlog
3
3
  psutil
4
4
  pybase64
5
5
  boto3
6
- cryptography==39.0.2
6
+ cryptography
7
7
  azure-identity
8
8
  azure-keyvault-secrets
9
- pykeystore
10
9
  pytest-mock
11
10
  pytest-watch
12
- pytest
11
+ pytest
12
+ watchdog
@@ -177,7 +177,7 @@ class Settings(dict):
177
177
  value: Value to set.
178
178
  store_name (str, optional): Name of the store for persistent settings.
179
179
  """
180
- self.secret_manager.set_secret(key, value, store_name)
180
+ self.secret_manager.set_persistent_setting(key, value)
181
181
 
182
182
  def __getattr__(self, key):
183
183
  """
@@ -153,6 +153,15 @@ class SecretsManager(object):
153
153
 
154
154
  return value
155
155
 
156
+ def set_persistent_setting(self, key, value):
157
+ for store in self.stores:
158
+ if 'User_Local_Store' == store.store_name:
159
+ if store.store_available and not store.store_read_only:
160
+ store.set_persistent_setting(key, value)
161
+ else:
162
+ logging.warning(f'Secrets Store {store.store_name} is either not available or is read only.')
163
+ break
164
+
156
165
  def set_secret(self, key, value, store_name='User_Local_Store'):
157
166
  """
158
167
  Set a secret in the specified secret store.
@@ -0,0 +1,196 @@
1
+ import os
2
+ import json
3
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
4
+ from cryptography.hazmat.primitives.hmac import HMAC
5
+ from cryptography.hazmat.primitives.hashes import SHA256
6
+ from cryptography.hazmat.backends import default_backend
7
+ from cryptography.fernet import Fernet
8
+ import base64
9
+ import logging
10
+
11
+ class PasswordProtectedKeystoreWithHMAC:
12
+ """
13
+ PasswordProtectedKeystoreWithHMAC provides a mechanism to securely store key-value pairs
14
+ in a file. The data is protected using encryption and integrity is ensured by HMAC.
15
+
16
+ Methods:
17
+ __init__(keystore_path, password):
18
+ Initializes the keystore with the provided path and password.
19
+
20
+ _derive_key(salt):
21
+ Derives an encryption key from the password and salt using PBKDF2.
22
+
23
+ _generate_hmac(data, key):
24
+ Generates an HMAC for the given data using the derived key.
25
+
26
+ _verify_hmac(data, key, expected_hmac):
27
+ Verifies the HMAC for the given data against the expected HMAC.
28
+
29
+ _load_keystore():
30
+ Loads the keystore from the file, verifying its integrity and returning the stored data.
31
+
32
+ _save_keystore(data):
33
+ Saves the provided data to the keystore file, including the generated HMAC for integrity.
34
+
35
+ set(key, value):
36
+ Adds or updates a key-value pair in the keystore.
37
+
38
+ get(key):
39
+ Retrieves a value from the keystore by its key.
40
+
41
+ delete(key):
42
+ Deletes a key-value pair from the keystore.
43
+ """
44
+ def __init__(self, keystore_path, password):
45
+ self.keystore_path = keystore_path
46
+ self.password = password.encode()
47
+
48
+ def _derive_key(self, salt):
49
+ """
50
+ Args:
51
+ salt: A cryptographic salt used for deriving the key, which should be a byte sequence.
52
+
53
+ Returns:
54
+ A base64-encoded, url-safe byte sequence representing the derived cryptographic key.
55
+ """
56
+ kdf = PBKDF2HMAC(
57
+ algorithm=SHA256(),
58
+ length=32,
59
+ salt=salt,
60
+ iterations=100_000,
61
+ backend=default_backend()
62
+ )
63
+ return base64.urlsafe_b64encode(kdf.derive(self.password))
64
+
65
+ def _generate_hmac(self, data, key):
66
+ """
67
+ Args:
68
+ data: The data that needs to be protected with an HMAC.
69
+ key: The cryptographic key used for HMAC generation.
70
+ """
71
+ hmac = HMAC(key, SHA256(), backend=default_backend())
72
+ hmac.update(data)
73
+ return hmac.finalize()
74
+
75
+ def _verify_hmac(self, data, key, expected_hmac):
76
+ """
77
+ Args:
78
+ data: The data for which the HMAC is to be verified.
79
+ key: The secret key used for the HMAC generation.
80
+ expected_hmac: The HMAC value that is expected for the provided data and key combination.
81
+
82
+ Returns:
83
+ bool: True if the computed HMAC matches the expected HMAC, False otherwise.
84
+
85
+ """
86
+ hmac = HMAC(key, SHA256(), backend=default_backend())
87
+ hmac.update(data)
88
+ try:
89
+ hmac.verify(expected_hmac)
90
+ return True
91
+ except:
92
+ return False
93
+
94
+ def _load_keystore(self):
95
+ """
96
+ Loads and decrypts the keystore data from a specified file path.
97
+
98
+ If the keystore file exists, it reads the file content and extracts the salt,
99
+ encrypted data, and HMAC. The method then derives the key using the extracted
100
+ salt and verifies the HMAC to ensure data integrity. If the HMAC verification
101
+ fails, a ValueError is raised indicating potential file tampering. After successful
102
+ verification, the method decrypts the data using the derived key and
103
+ returns the decrypted JSON content. If the file does not exist, an empty
104
+ dictionary is returned.
105
+
106
+ Raises:
107
+ ValueError: If HMAC verification fails, indicating possible tampering.
108
+
109
+ Returns:
110
+ dict: The decrypted content of the keystore file as a dictionary, or an
111
+ empty dictionary if the keystore file does not exist.
112
+ """
113
+ if os.path.exists(self.keystore_path):
114
+ with open(self.keystore_path, 'rb') as file:
115
+ # Read the entire file content
116
+ file_content = file.read()
117
+
118
+ # Extract the salt, encrypted data, and HMAC
119
+ salt = file_content[:16] # First 16 bytes
120
+ encrypted_data = file_content[16:-32] # Middle bytes
121
+ stored_hmac = file_content[-32:] # Last 32 bytes
122
+
123
+ # Derive the key and verify the HMAC
124
+ derived_key = self._derive_key(salt)
125
+ if not self._verify_hmac(salt + encrypted_data, derived_key, stored_hmac):
126
+ raise ValueError("HMAC verification failed: File may have been tampered with.")
127
+
128
+ # Decrypt the data
129
+ cipher_suite = Fernet(derived_key)
130
+ decrypted_data = cipher_suite.decrypt(encrypted_data)
131
+ return json.loads(decrypted_data.decode())
132
+ return {}
133
+
134
+ def _save_keystore(self, data):
135
+ """
136
+ Args:
137
+ data: The data to be saved in the keystore, which will be encrypted and stored securely.
138
+ """
139
+ # Generate a random 16-byte salt
140
+ salt = os.urandom(16)
141
+
142
+ # Derive the key
143
+ derived_key = self._derive_key(salt)
144
+ cipher_suite = Fernet(derived_key)
145
+
146
+ # Encrypt the data
147
+ encrypted_data = cipher_suite.encrypt(json.dumps(data).encode())
148
+
149
+ # Generate the HMAC
150
+ hmac = self._generate_hmac(salt + encrypted_data, derived_key)
151
+
152
+ # Write the salt, encrypted data, and HMAC to the file
153
+ with open(self.keystore_path, 'wb') as file:
154
+ file.write(salt)
155
+ file.write(encrypted_data)
156
+ file.write(hmac)
157
+
158
+ def set(self, key, value):
159
+ """
160
+ Args:
161
+ key: The key to be stored in the keystore.
162
+ value: The value associated with the key to be stored.
163
+ """
164
+ keystore = self._load_keystore()
165
+ keystore[key] = value
166
+ self._save_keystore(keystore)
167
+ logging.debug(f"Key '{key}' stored successfully.")
168
+
169
+ def get(self, key):
170
+ """
171
+ Retrieves the value associated with the specified key from the keystore.
172
+
173
+ Args:
174
+ key: The key whose associated value is to be returned.
175
+
176
+ Returns:
177
+ The value corresponding to the specified key if it exists, otherwise None.
178
+ """
179
+ keystore = self._load_keystore()
180
+ logging.debug(f"Retrieving Key '{key}'")
181
+ return keystore.get(key)
182
+
183
+ def delete(self, key):
184
+ """
185
+ Deletes the specified key from the keystore if it exists.
186
+
187
+ Args:
188
+ key: The key to be deleted from the keystore.
189
+ """
190
+ keystore = self._load_keystore()
191
+ if key in keystore:
192
+ del keystore[key]
193
+ self._save_keystore(keystore)
194
+ logging.debug(f"Key '{key}' deleted successfully.")
195
+ else:
196
+ logging.debug(f"Key '{key}' not found in the keystore.")
@@ -8,7 +8,9 @@ import pybase64
8
8
  from .secret_store import AbstractSecretStore, SecretsStoreException
9
9
  from itertools import cycle
10
10
  from ...misc import run_cmd
11
- import pykeystore
11
+
12
+ from .keystore import PasswordProtectedKeystoreWithHMAC
13
+
12
14
  from base64 import urlsafe_b64encode
13
15
 
14
16
 
@@ -48,42 +50,23 @@ class LocalSecretStore(AbstractSecretStore):
48
50
  password (str): Password for the local secret store (default: None).
49
51
  """
50
52
  super().__init__(store_name, 'local', store_priority, application_settings)
51
- self.store_path = os.path.join(root_store_path, f"{app_short_name}.keystore")
53
+ if os.path.exists(os.path.join(root_store_path, f"{app_short_name}.keystore")):
54
+ logging.warning(f'Old Keystore file "{os.path.join(root_store_path, f"{app_short_name}.keystore")}" is no longer supported.')
55
+ self.store_path = os.path.join(root_store_path, f"{app_short_name}.v2keystore")
52
56
 
53
57
  # If password is not provided, generate a unique password
54
58
  if password is None:
55
59
  password = self.__guid()
56
60
 
57
61
  try:
58
- # If the store does not exist, initialize it
59
- if not os.path.exists(self.store_path):
60
- self.store = self.__initialise_secrets_store(password)
61
-
62
62
  # Try to load the existing store
63
- self.store = pykeystore.KeyStoreEx.load(self.store_path, password)
63
+ self.store = PasswordProtectedKeystoreWithHMAC(self.store_path, password)
64
64
  self.store_available = True
65
65
  self.store_read_only = not self.__is_writeable()
66
66
  logging.info(f'Successfully opened Secrets Store: {self.store_path}')
67
67
  except Exception as ex:
68
68
  raise SecretsStoreException(f'Failed to open Secrets Store: {self.store_path}. Error: {str(ex)}')
69
69
 
70
- def __initialise_secrets_store(self, password):
71
- """
72
- Initialize the secrets store if it doesn't exist.
73
-
74
- Args:
75
- password (str): Password for the local secret store.
76
-
77
- Returns:
78
- Initialized secrets store.
79
- """
80
- try:
81
- store = pykeystore.KeyStoreEx.create(self.store_path, password)
82
- return store
83
- except Exception as ex:
84
- logging.error(f'Failed to create Secrets Store. Error: {str(ex)}')
85
- raise ex
86
-
87
70
  def __guid(self):
88
71
  """
89
72
  Generate a unique identifier based on the machine and store path.
@@ -115,7 +98,7 @@ class LocalSecretStore(AbstractSecretStore):
115
98
  base += self.store_path
116
99
  key = re.sub("[^a-zA-Z]+", "", base)
117
100
  xored = ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(base, cycle(key)))
118
- return urlsafe_b64encode(pybase64.b64encode_as_string(xored.encode())[:32].encode())
101
+ return urlsafe_b64encode(pybase64.b64encode_as_string(xored.encode())[:32].encode()).decode()
119
102
 
120
103
  def get_secret(self, key, default_value=None):
121
104
  """
@@ -128,12 +111,20 @@ class LocalSecretStore(AbstractSecretStore):
128
111
  Returns:
129
112
  Secret value if found, else default_value.
130
113
  """
131
- entry = self.store.getPassword(account=key)
114
+ entry = self.store.get(key=key)
132
115
  if not entry or entry == 'NONE':
133
116
  return default_value
134
117
 
135
118
  return entry
136
119
 
120
+ def set_persistent_setting(self, key, value):
121
+ if self.get_secret(key):
122
+ self.delete_secret(key)
123
+
124
+ self.store.set(key=key, value=value)
125
+ self.__save()
126
+
127
+
137
128
  def set_secret(self, key, value):
138
129
  """
139
130
  Set a secret in the local secret store.
@@ -145,7 +136,7 @@ class LocalSecretStore(AbstractSecretStore):
145
136
  if self.get_secret(key):
146
137
  self.delete_secret(key)
147
138
 
148
- self.store.setPassword(account=key, password=value)
139
+ self.store.set(key=key, value=value)
149
140
  self.__save()
150
141
  index = self.get_index()
151
142
  if key not in index:
@@ -159,7 +150,7 @@ class LocalSecretStore(AbstractSecretStore):
159
150
  Args:
160
151
  key (str): Key of the secret.
161
152
  """
162
- entry = self.store.setPassword(account=key, password='NONE')
153
+ self.store.delete(key=key)
163
154
  self.__save()
164
155
  index = self.get_index()
165
156
  while key in index:
@@ -168,7 +159,7 @@ class LocalSecretStore(AbstractSecretStore):
168
159
 
169
160
  def __set_index(self, index: list):
170
161
  logging.info(index)
171
- self.store.setPassword(account=f'{self.store_name}.INDEX', password=json.dumps(index))
162
+ self.store.set(key=f'{self.store_name}.INDEX', value=json.dumps(index))
172
163
  self.__save()
173
164
 
174
165
  def get_index(self) -> list:
@@ -181,7 +172,8 @@ class LocalSecretStore(AbstractSecretStore):
181
172
 
182
173
  def __save(self):
183
174
  """Save the changes made to the local secret store."""
184
- self.store.save(self.store_path, self.__guid())
175
+ self.store.set('sstore_save', 'true')
176
+ self.store.delete('sstore_save')
185
177
 
186
178
  def __is_writeable(self):
187
179
  try:
@@ -0,0 +1,155 @@
1
+ from watchdog.observers import Observer
2
+ from watchdog.events import FileSystemEventHandler, FileSystemEvent
3
+ import yaml
4
+ import logging
5
+ import os
6
+ import hashlib
7
+
8
+ class ConfigFileWatcher(FileSystemEventHandler):
9
+
10
+ def __init__(self, change_action, delete_action, watch_file, watch_folder):
11
+ self.change_action = change_action
12
+ self.delete_action = delete_action
13
+ self.watch_file = watch_file
14
+ self.watch_folder = watch_folder
15
+ self.watch_file_sha256 = self.calculate_sha256(os.path.join(self.watch_folder, self.watch_file))
16
+
17
+ def calculate_sha256(self, file_path):
18
+ sha256_hash = hashlib.sha256()
19
+ try:
20
+ with open(file_path, "rb") as f:
21
+ # Read and update hash in chunks of 4K
22
+ for byte_block in iter(lambda: f.read(4096), b""):
23
+ sha256_hash.update(byte_block)
24
+ return sha256_hash.hexdigest()
25
+ except FileNotFoundError:
26
+ return "File not found."
27
+ except Exception as e:
28
+ return f"An error occurred: {str(e)}"
29
+
30
+ def process(self, event: FileSystemEvent):
31
+ """
32
+ event.event_type
33
+ 'modified' | 'created' | 'moved' | 'deleted'
34
+ event.src_path
35
+ path to the modified file
36
+ """
37
+ if event.src_path.endswith(self.watch_file):
38
+ if event.event_type == 'deleted':
39
+ logging.warning(f'Config Watch File Deleted: {event.src_path}')
40
+ self.delete_action()
41
+ elif event.event_type == 'modified':
42
+ if self.calculate_sha256(event.src_path) != self.watch_file_sha256:
43
+ logging.warning(f'Config Watch File Changed: {event.src_path}')
44
+ self.watch_file_sha256 = self.calculate_sha256(event.src_path)
45
+ self.change_action()
46
+ elif event.event_type == 'created':
47
+ logging.warning(f'Config Watch File Created: {event.src_path}')
48
+ self.watch_file_sha256 = self.calculate_sha256(event.src_path)
49
+ self.change_action()
50
+
51
+
52
+ def on_deleted(self, event: FileSystemEvent) -> None:
53
+ self.process(event)
54
+
55
+ def on_modified(self, event: FileSystemEvent):
56
+ self.process(event)
57
+
58
+ def on_created(self, event: FileSystemEvent):
59
+ self.process(event)
60
+
61
+
62
+
63
+ class SettingsReader(dict):
64
+ """
65
+ A class for reading settings from a YAML file and accessing them using dot notation.
66
+
67
+ Attributes:
68
+ path (str): Path to the directory containing the settings YAML file.
69
+ priority (int): Priority of the settings reader.
70
+ settings_file (str): Full path to the settings YAML file.
71
+ """
72
+ CONFIG_FILE = "config.yaml"
73
+
74
+
75
+ def __init__(self, path: str, priority: int) -> None:
76
+ """
77
+ Initialize the SettingsReader.
78
+
79
+ Args:
80
+ path (str): Path to the directory containing the settings YAML file.
81
+ priority (int): Priority of the settings reader.
82
+ """
83
+ self.priority = priority
84
+ self.settings_file = os.path.join(path, self.CONFIG_FILE)
85
+
86
+ self.load_yaml_file()
87
+ self.observer = Observer()
88
+ event_handler = ConfigFileWatcher(change_action=self.load_yaml_file, delete_action=super().clear,
89
+ watch_file=self.CONFIG_FILE, watch_folder=path)
90
+ self.observer.schedule(event_handler, path, recursive=False)
91
+ self.observer.start()
92
+
93
+ super().__init__()
94
+
95
+ def load_yaml_file(self):
96
+ """
97
+ Load settings from the YAML file and update the dictionary.
98
+ """
99
+ if os.path.exists(self.settings_file):
100
+ try:
101
+ with open(self.settings_file, 'r', encoding='UTF-8') as file:
102
+ super().clear()
103
+ self.update(yaml.safe_load(file))
104
+ logging.info(f'Loaded settings file {self.settings_file}.')
105
+ except Exception as ex:
106
+ logging.error(f'Error reading in settings file {self.settings_file}. {str(ex)}')
107
+ print(f'Error reading in settings file {self.settings_file}. {str(ex)}')
108
+ else:
109
+ logging.warning(f'Settings file "{self.settings_file}" does not exist.')
110
+
111
+ def clear(self) -> None:
112
+ """
113
+ Clear method not implemented.
114
+ """
115
+ raise NotImplementedError
116
+
117
+ def popitem(self):
118
+ """
119
+ popitem method not implemented.
120
+ """
121
+ raise NotImplementedError
122
+
123
+ def __setitem__(self, k, v) -> None:
124
+ """
125
+ __setitem__ method not implemented.
126
+ """
127
+ raise NotImplementedError
128
+
129
+ def pop(self, __key):
130
+ """
131
+ pop method not implemented.
132
+ """
133
+ raise NotImplementedError
134
+
135
+ def __getitem__(self, key):
136
+ """
137
+ Get item from the dictionary using dot notation.
138
+
139
+ Args:
140
+ key (str): Key in dot notation.
141
+
142
+ Returns:
143
+ The value associated with the key.
144
+ """
145
+ keys = key.split('.')
146
+ if len(keys) == 1:
147
+ return dict.__getitem__(self, key)
148
+ else:
149
+ data = self.copy()
150
+ for key in keys:
151
+ if key in data:
152
+ data = data[key]
153
+ else:
154
+ return None
155
+ return data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dtPyAppFramework
3
- Version: 1.5.2
3
+ Version: 2.0
4
4
  Summary: A Python library for common features in application development.
5
5
  Author-email: Digital-Thought <dev@digital-thought.org>
6
6
  Maintainer-email: Digital-Thought <dev@digital-thought.org>
@@ -54,13 +54,13 @@ Requires-Dist: colorlog
54
54
  Requires-Dist: psutil
55
55
  Requires-Dist: pybase64
56
56
  Requires-Dist: boto3
57
- Requires-Dist: cryptography==39.0.2
57
+ Requires-Dist: cryptography
58
58
  Requires-Dist: azure-identity
59
59
  Requires-Dist: azure-keyvault-secrets
60
- Requires-Dist: pykeystore
61
60
  Requires-Dist: pytest-mock
62
61
  Requires-Dist: pytest-watch
63
62
  Requires-Dist: pytest
63
+ Requires-Dist: watchdog
64
64
 
65
65
  # dtPyAppFramework
66
66
 
@@ -35,5 +35,8 @@ src/dtPyAppFramework/settings/settings_reader.py
35
35
  src/dtPyAppFramework/settings/secrets/__init__.py
36
36
  src/dtPyAppFramework/settings/secrets/aws_secret_store.py
37
37
  src/dtPyAppFramework/settings/secrets/azure_secret_store.py
38
+ src/dtPyAppFramework/settings/secrets/keystore.py
38
39
  src/dtPyAppFramework/settings/secrets/local_secret_store.py
39
- src/dtPyAppFramework/settings/secrets/secret_store.py
40
+ src/dtPyAppFramework/settings/secrets/secret_store.py
41
+ tests/test_keystore.py
42
+ tests/test_settings_reader.py
@@ -3,10 +3,10 @@ colorlog
3
3
  psutil
4
4
  pybase64
5
5
  boto3
6
- cryptography==39.0.2
6
+ cryptography
7
7
  azure-identity
8
8
  azure-keyvault-secrets
9
- pykeystore
10
9
  pytest-mock
11
10
  pytest-watch
12
11
  pytest
12
+ watchdog
@@ -0,0 +1,70 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ from unittest import mock
5
+
6
+ import pytest
7
+ from cryptography.fernet import Fernet
8
+ from cryptography.hazmat.backends import default_backend
9
+ from cryptography.hazmat.primitives.hashes import SHA256
10
+ from cryptography.hazmat.primitives.hmac import HMAC
11
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
12
+ # Import the class to test
13
+ from dtPyAppFramework.settings.secrets.keystore import PasswordProtectedKeystoreWithHMAC
14
+
15
+ # The path to a temporary file for testing purposes
16
+ TEMP_FILE_PATH = "/tmp/test_keystore.db"
17
+
18
+
19
+ def teardown_function(function):
20
+ if os.path.exists(TEMP_FILE_PATH):
21
+ os.remove(TEMP_FILE_PATH)
22
+
23
+
24
+ @pytest.fixture
25
+ def keystore():
26
+ return PasswordProtectedKeystoreWithHMAC(TEMP_FILE_PATH, 'test_password')
27
+
28
+
29
+ def test_init(keystore):
30
+ assert keystore.keystore_path == TEMP_FILE_PATH
31
+ assert keystore.password == 'test_password'.encode()
32
+
33
+
34
+ def test_derive_key(keystore):
35
+ salt = os.urandom(16)
36
+ key = keystore._derive_key(salt)
37
+ assert (isinstance(key, bytes))
38
+ assert (len(key) == 44) # Length of base64 encoded 256-bit key
39
+
40
+
41
+ def test_generate_hmac(keystore):
42
+ salt = os.urandom(16)
43
+ key = keystore._derive_key(salt)
44
+ data = os.urandom(16)
45
+ hmac = keystore._generate_hmac(data, key)
46
+ assert (isinstance(hmac, bytes))
47
+ assert (len(hmac) == 32) # Length of SHA256 hash
48
+
49
+
50
+ def test_verify_hmac(keystore):
51
+ salt = os.urandom(16)
52
+ key = keystore._derive_key(salt)
53
+ data = os.urandom(16)
54
+ hmac = keystore._generate_hmac(data, key)
55
+ assert keystore._verify_hmac(data, key, hmac) is True
56
+
57
+
58
+ def test_save_and_load_keystore(keystore):
59
+ test_data = {"test_key": "test_value"}
60
+ keystore._save_keystore(test_data)
61
+ loaded_data = keystore._load_keystore()
62
+
63
+ assert loaded_data == test_data
64
+
65
+
66
+ def test_set_get_delete_methods(keystore):
67
+ keystore.set("test_key", "test_value")
68
+ assert keystore.get("test_key") == "test_value"
69
+ keystore.delete("test_key")
70
+ assert keystore.get("test_key") is None
@@ -0,0 +1,54 @@
1
+ import os
2
+ from unittest import mock
3
+
4
+ import pytest
5
+ from dtPyAppFramework.settings.settings_reader import SettingsReader
6
+
7
+
8
+ # Test that __init__ sets the attributes correctly
9
+ def test_init():
10
+ settings_reader = SettingsReader('/path/to/settings', 5)
11
+ assert settings_reader.priority == 5
12
+ assert settings_reader.settings_file == '/path/to/settings/config.yaml'
13
+
14
+
15
+ # Test that load_yaml_file loads a YAML file correctly
16
+ @mock.patch('yaml.safe_load', return_value={'key': 'value'})
17
+ def test_load_yaml_file(mock_safe_load):
18
+ settings_reader = SettingsReader('/path/to/settings', 5)
19
+ settings_reader.load_yaml_file()
20
+ assert settings_reader.get('key') == 'value'
21
+
22
+
23
+ # Test that __getitem__ can retrieve data correctly using dot notation
24
+ def test_getitem():
25
+ settings_reader = SettingsReader('/path/to/settings', 5)
26
+
27
+ data = {
28
+ 'key': 'value',
29
+ 'nested': {
30
+ 'key': 'nested value'
31
+ }
32
+ }
33
+
34
+ settings_reader.update(data)
35
+
36
+ assert settings_reader.__getitem__('nested.key') == 'nested value'
37
+ assert settings_reader.__getitem__('key') == 'value'
38
+
39
+
40
+ # Test that methods that are not implemented raise a NotImplementedError
41
+ def test_not_implemented_methods():
42
+ settings_reader = SettingsReader('/path/to/settings', 5)
43
+
44
+ with pytest.raises(NotImplementedError):
45
+ settings_reader.clear()
46
+
47
+ with pytest.raises(NotImplementedError):
48
+ settings_reader.popitem()
49
+
50
+ with pytest.raises(NotImplementedError):
51
+ settings_reader.__setitem__('key', 'value')
52
+
53
+ with pytest.raises(NotImplementedError):
54
+ settings_reader.pop('key')
@@ -1,88 +0,0 @@
1
- import json
2
- import yaml
3
- import logging
4
- import os
5
-
6
- class SettingsReader(dict):
7
- """
8
- A class for reading settings from a YAML file and accessing them using dot notation.
9
-
10
- Attributes:
11
- path (str): Path to the directory containing the settings YAML file.
12
- priority (int): Priority of the settings reader.
13
- settings_file (str): Full path to the settings YAML file.
14
- """
15
-
16
- def __init__(self, path: str, priority: int) -> None:
17
- """
18
- Initialize the SettingsReader.
19
-
20
- Args:
21
- path (str): Path to the directory containing the settings YAML file.
22
- priority (int): Priority of the settings reader.
23
- """
24
- self.priority = priority
25
- self.settings_file = os.path.join(path, "config.yaml")
26
-
27
- if os.path.exists(self.settings_file):
28
- self.load_yaml_file()
29
-
30
- super().__init__()
31
-
32
- def load_yaml_file(self):
33
- """
34
- Load settings from the YAML file and update the dictionary.
35
- """
36
- try:
37
- with open(self.settings_file, 'r', encoding='UTF-8') as file:
38
- self.update(yaml.safe_load(file))
39
- except Exception as ex:
40
- logging.error(f'Error reading in settings file {self.settings_file}. {str(ex)}')
41
- print(f'Error reading in settings file {self.settings_file}. {str(ex)}')
42
-
43
-
44
- def clear(self) -> None:
45
- """
46
- Clear method not implemented.
47
- """
48
- raise NotImplementedError
49
-
50
- def popitem(self):
51
- """
52
- popitem method not implemented.
53
- """
54
- raise NotImplementedError
55
-
56
- def __setitem__(self, k, v) -> None:
57
- """
58
- __setitem__ method not implemented.
59
- """
60
- raise NotImplementedError
61
-
62
- def pop(self, __key):
63
- """
64
- pop method not implemented.
65
- """
66
- raise NotImplementedError
67
-
68
- def __getitem__(self, key):
69
- """
70
- Get item from the dictionary using dot notation.
71
-
72
- Args:
73
- key (str): Key in dot notation.
74
-
75
- Returns:
76
- The value associated with the key.
77
- """
78
- keys = key.split('.')
79
- if len(keys) == 1:
80
- return dict.__getitem__(self, key)
81
- else:
82
- data = self.copy()
83
- for key in keys:
84
- if key in data:
85
- data = data[key]
86
- else:
87
- return None
88
- return data