xenoslib 0.1.30__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- xenoslib/about.py +1 -1
- xenoslib/base.py +62 -0
- xenoslib/tools/config_loader.py +144 -65
- {xenoslib-0.1.30.dist-info → xenoslib-0.3.0.dist-info}/METADATA +1 -1
- {xenoslib-0.1.30.dist-info → xenoslib-0.3.0.dist-info}/RECORD +8 -8
- {xenoslib-0.1.30.dist-info → xenoslib-0.3.0.dist-info}/WHEEL +0 -0
- {xenoslib-0.1.30.dist-info → xenoslib-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {xenoslib-0.1.30.dist-info → xenoslib-0.3.0.dist-info}/top_level.txt +0 -0
xenoslib/about.py
CHANGED
xenoslib/base.py
CHANGED
|
@@ -4,6 +4,68 @@ import argparse
|
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init_logger(
|
|
14
|
+
use_file: bool = True,
|
|
15
|
+
level: int = logging.INFO,
|
|
16
|
+
backup_count: int = 0, # New parameter: number of log files to retain (0 means keep all)
|
|
17
|
+
) -> logging.Logger:
|
|
18
|
+
"""
|
|
19
|
+
Automatically names logger after caller's filename, rotates by time, and retains specified number of log files
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
use_file: Whether to enable file logging
|
|
23
|
+
level: Logging level (DEBUG/INFO/WARN/ERROR)
|
|
24
|
+
backup_count: Number of historical log files to retain (default 0=keep all)
|
|
25
|
+
"""
|
|
26
|
+
# 1. Dynamically get caller's filename
|
|
27
|
+
caller_frame = inspect.stack()[1]
|
|
28
|
+
caller_path = caller_frame.filename
|
|
29
|
+
caller_name = os.path.splitext(os.path.basename(caller_path))[0]
|
|
30
|
+
|
|
31
|
+
# 2. Configure log directory and filename
|
|
32
|
+
log_dir = "logs"
|
|
33
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
34
|
+
log_filename = f"{caller_name}.log"
|
|
35
|
+
full_path = os.path.join(log_dir, log_filename)
|
|
36
|
+
|
|
37
|
+
# 3. Avoid duplicate initialization
|
|
38
|
+
logger = logging.getLogger(caller_name)
|
|
39
|
+
if logger.hasHandlers():
|
|
40
|
+
return logger
|
|
41
|
+
|
|
42
|
+
logger.setLevel(level)
|
|
43
|
+
formatter = logging.Formatter(
|
|
44
|
+
"%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s",
|
|
45
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# 4. Console handler (required)
|
|
49
|
+
console_handler = logging.StreamHandler()
|
|
50
|
+
console_handler.setFormatter(formatter)
|
|
51
|
+
handlers = [console_handler]
|
|
52
|
+
|
|
53
|
+
# 5. File handler (daily rotation + file retention policy)
|
|
54
|
+
if use_file:
|
|
55
|
+
file_handler = TimedRotatingFileHandler(
|
|
56
|
+
full_path,
|
|
57
|
+
when="midnight", # Rotate daily
|
|
58
|
+
interval=1,
|
|
59
|
+
backupCount=backup_count, # Key parameter: controls number of retained files
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
)
|
|
62
|
+
file_handler.setFormatter(formatter)
|
|
63
|
+
handlers.append(file_handler)
|
|
64
|
+
|
|
65
|
+
for handler in handlers:
|
|
66
|
+
logger.addHandler(handler)
|
|
67
|
+
|
|
68
|
+
return logger
|
|
7
69
|
|
|
8
70
|
|
|
9
71
|
def sleep(seconds, mute=False):
|
xenoslib/tools/config_loader.py
CHANGED
|
@@ -28,36 +28,37 @@ class ConfigLoader(SingletonWithArgs):
|
|
|
28
28
|
|
|
29
29
|
# With Vault (hvac imported on demand)
|
|
30
30
|
>>> config = ConfigLoader("config.yml", vault_secret_id="my-secret-id")
|
|
31
|
+
|
|
32
|
+
# Write to Vault using attribute style
|
|
33
|
+
>>> config.test_section.test_key = "new_value"
|
|
34
|
+
|
|
35
|
+
# Write to Vault using dictionary style
|
|
36
|
+
>>> config["test_section"]["test_key"] = "new_value"
|
|
31
37
|
"""
|
|
32
38
|
|
|
39
|
+
VAULT_SUFFIX = "@vault"
|
|
40
|
+
KV_MOUNT_POINT = "kv"
|
|
41
|
+
|
|
33
42
|
cache = {}
|
|
34
43
|
vault_client = None
|
|
35
44
|
|
|
36
45
|
def __init__(self, config_file_path="config.yml", vault_secret_id=None):
|
|
37
46
|
"""Initialize the ConfigLoader with a configuration file and optional Vault secret."""
|
|
38
47
|
with open(config_file_path, "r") as f:
|
|
39
|
-
|
|
48
|
+
config_data = yaml.safe_load(f)
|
|
49
|
+
self._raw_config = config_data if isinstance(config_data, dict) else {}
|
|
40
50
|
|
|
41
51
|
if vault_secret_id is not None:
|
|
42
52
|
self.vault_secret_id = vault_secret_id
|
|
43
53
|
self._check_and_renew_vault_client()
|
|
44
54
|
|
|
45
55
|
def _init_vault_client(self):
|
|
46
|
-
"""Initialize and authenticate the Vault client (imports hvac on demand).
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
vault_secret_id (str): Secret ID for Vault authentication.
|
|
50
|
-
|
|
51
|
-
Raises:
|
|
52
|
-
ImportError: If hvac package is not installed.
|
|
53
|
-
KeyError: If required Vault configuration is missing.
|
|
54
|
-
Exception: If Vault authentication fails.
|
|
55
|
-
"""
|
|
56
|
+
"""Initialize and authenticate the Vault client (imports hvac on demand)."""
|
|
56
57
|
try:
|
|
57
58
|
import hvac # Lazy import
|
|
58
59
|
except ImportError as e:
|
|
59
60
|
raise ImportError(
|
|
60
|
-
"hvac package is required for Vault integration.
|
|
61
|
+
"hvac package is required for Vault integration. Install with: pip install hvac"
|
|
61
62
|
) from e
|
|
62
63
|
|
|
63
64
|
try:
|
|
@@ -75,84 +76,113 @@ class ConfigLoader(SingletonWithArgs):
|
|
|
75
76
|
)
|
|
76
77
|
except Exception as e:
|
|
77
78
|
self.vault_client = None
|
|
78
|
-
raise Exception(f"Failed to initialize Vault client: {str(e)}")
|
|
79
|
+
raise Exception(f"Failed to initialize Vault client: {str(e)}") from e
|
|
79
80
|
|
|
80
81
|
def _check_and_renew_vault_client(self):
|
|
81
|
-
# 检查当前Token的状态,包括过期时间和可续租性
|
|
82
82
|
if not self.vault_client or not self.vault_client.is_authenticated():
|
|
83
|
-
# 如果当前Token无效,则重新认证
|
|
84
83
|
self._init_vault_client()
|
|
85
84
|
|
|
85
|
+
def _is_vault_reference(self, section_config, key_name):
|
|
86
|
+
"""检查键是否是Vault引用"""
|
|
87
|
+
return f"{key_name}{self.VAULT_SUFFIX}" in section_config
|
|
88
|
+
|
|
86
89
|
def get(self, section, key_name, use_cache=True):
|
|
87
|
-
"""Retrieve a configuration value.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
section (str): The configuration section name.
|
|
91
|
-
key_name (str): The key name within the section.
|
|
92
|
-
use_cache (bool): Whether to use cached values. Defaults to True.
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
The configuration value, which may come from:
|
|
96
|
-
- Direct configuration value
|
|
97
|
-
- Vault secret (if Vault is initialized)
|
|
98
|
-
- Cache (if enabled)
|
|
99
|
-
|
|
100
|
-
Raises:
|
|
101
|
-
KeyError: If the section or key is not found.
|
|
102
|
-
Exception: If Vault access is required but not available.
|
|
103
|
-
"""
|
|
104
|
-
if section not in self._raw_config:
|
|
90
|
+
"""Retrieve a configuration value."""
|
|
91
|
+
section_config = self._raw_config.get(section)
|
|
92
|
+
if section_config is None:
|
|
105
93
|
raise KeyError(f"Section '{section}' not found")
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return self._raw_config[section][key_name]
|
|
95
|
+
if key_name in section_config:
|
|
96
|
+
return section_config[key_name]
|
|
110
97
|
|
|
111
|
-
|
|
112
|
-
vault_key = f"{key_name}@vault"
|
|
113
|
-
if vault_key in self._raw_config[section]:
|
|
98
|
+
if self._is_vault_reference(section_config, key_name):
|
|
114
99
|
if self.vault_client is None:
|
|
115
100
|
raise Exception(
|
|
116
101
|
f"Vault access required for {key_name} but Vault is not initialized"
|
|
117
102
|
)
|
|
118
103
|
|
|
119
|
-
cache_key = f"{section}
|
|
120
|
-
|
|
104
|
+
cache_key = f"{section}:{key_name}"
|
|
121
105
|
if use_cache and cache_key in self.cache:
|
|
122
106
|
return self.cache[cache_key]
|
|
123
107
|
value = self._get_value_from_vault(section, key_name)
|
|
124
108
|
self.cache[cache_key] = value
|
|
125
109
|
return value
|
|
126
110
|
|
|
127
|
-
raise KeyError(f"Key '{key_name}'
|
|
111
|
+
raise KeyError(f"Key '{key_name}' not found in section '{section}'")
|
|
128
112
|
|
|
129
|
-
def
|
|
130
|
-
"""
|
|
113
|
+
def set(self, section, key_name, value, use_cache=True):
|
|
114
|
+
"""Set a configuration value to Vault."""
|
|
115
|
+
section_config = self._raw_config.get(section)
|
|
116
|
+
if section_config is None:
|
|
117
|
+
raise KeyError(f"Section '{section}' not found")
|
|
131
118
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
key_name (str): The key name within the section.
|
|
119
|
+
if not self._is_vault_reference(section_config, key_name):
|
|
120
|
+
raise KeyError(f"Key '{key_name}' is not a Vault reference in section '{section}'")
|
|
135
121
|
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
if self.vault_client is None:
|
|
123
|
+
raise Exception(f"Vault access required for {key_name} but Vault is not initialized")
|
|
138
124
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
""
|
|
125
|
+
self._set_value_to_vault(section, key_name, value)
|
|
126
|
+
|
|
127
|
+
cache_key = f"{section}:{key_name}"
|
|
128
|
+
if use_cache:
|
|
129
|
+
self.cache[cache_key] = value
|
|
130
|
+
|
|
131
|
+
def _get_value_from_vault(self, section, key_name):
|
|
132
|
+
"""Retrieve a secret value from Vault."""
|
|
142
133
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
134
|
+
section_config = self._raw_config[section]
|
|
135
|
+
vault_path = section_config.get("vault_path")
|
|
136
|
+
if not vault_path:
|
|
137
|
+
raise KeyError(f"Missing vault_path in section '{section}'")
|
|
138
|
+
|
|
139
|
+
vault_key_ref = f"{key_name}{self.VAULT_SUFFIX}"
|
|
140
|
+
vault_key = section_config[vault_key_ref]
|
|
141
|
+
|
|
142
|
+
namespace = section_config.get("vault_namespace") or self._raw_config["vault"]["space"]
|
|
143
|
+
self.vault_client.adapter.namespace = namespace
|
|
144
|
+
|
|
150
145
|
data = self.vault_client.secrets.kv.read_secret_version(
|
|
151
|
-
path=vault_path, mount_point=
|
|
146
|
+
path=vault_path, mount_point=self.KV_MOUNT_POINT, raise_on_deleted_version=True
|
|
152
147
|
)
|
|
153
148
|
return data["data"]["data"][vault_key]
|
|
154
149
|
except Exception as e:
|
|
155
|
-
raise Exception(f"Failed to fetch {key_name} from Vault: {str(e)}")
|
|
150
|
+
raise Exception(f"Failed to fetch {key_name} from Vault: {str(e)}") from e
|
|
151
|
+
|
|
152
|
+
def _set_value_to_vault(self, section, key_name, value):
|
|
153
|
+
"""Set a secret value to Vault."""
|
|
154
|
+
try:
|
|
155
|
+
self._check_and_renew_vault_client()
|
|
156
|
+
|
|
157
|
+
section_config = self._raw_config[section]
|
|
158
|
+
vault_path = section_config.get("vault_path")
|
|
159
|
+
if not vault_path:
|
|
160
|
+
raise KeyError(f"Missing vault_path in section '{section}'")
|
|
161
|
+
|
|
162
|
+
vault_key_ref = f"{key_name}{self.VAULT_SUFFIX}"
|
|
163
|
+
vault_key = section_config[vault_key_ref]
|
|
164
|
+
|
|
165
|
+
namespace = section_config.get("vault_namespace") or self._raw_config["vault"]["space"]
|
|
166
|
+
self.vault_client.adapter.namespace = namespace
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
data = self.vault_client.secrets.kv.read_secret_version(
|
|
170
|
+
path=vault_path, mount_point=self.KV_MOUNT_POINT, raise_on_deleted_version=True
|
|
171
|
+
)
|
|
172
|
+
secret_data = data["data"]["data"]
|
|
173
|
+
except Exception:
|
|
174
|
+
logger.warning(f"Secret not found, creating new secret at {vault_path}")
|
|
175
|
+
secret_data = {}
|
|
176
|
+
|
|
177
|
+
secret_data[vault_key] = value
|
|
178
|
+
|
|
179
|
+
self.vault_client.secrets.kv.create_or_update_secret(
|
|
180
|
+
path=vault_path, secret=secret_data, mount_point=self.KV_MOUNT_POINT
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
logger.info(f"Updated Vault secret: {vault_path}/{vault_key}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise Exception(f"Failed to set {key_name} to Vault: {str(e)}") from e
|
|
156
186
|
|
|
157
187
|
def __getitem__(self, section):
|
|
158
188
|
"""Dictionary-style access to configuration sections."""
|
|
@@ -160,6 +190,10 @@ class ConfigLoader(SingletonWithArgs):
|
|
|
160
190
|
raise KeyError(f"Section '{section}' not found")
|
|
161
191
|
return SectionProxy(self, section)
|
|
162
192
|
|
|
193
|
+
def __setitem__(self, section, proxy):
|
|
194
|
+
"""Prevent direct assignment to sections."""
|
|
195
|
+
raise TypeError("ConfigLoader does not support direct section assignment")
|
|
196
|
+
|
|
163
197
|
def __getattr__(self, section):
|
|
164
198
|
"""Attribute-style access to configuration sections."""
|
|
165
199
|
try:
|
|
@@ -179,6 +213,10 @@ class SectionProxy:
|
|
|
179
213
|
"""Dictionary-style access to configuration values."""
|
|
180
214
|
return self._loader.get(self._section, key)
|
|
181
215
|
|
|
216
|
+
def __setitem__(self, key, value):
|
|
217
|
+
"""Dictionary-style setting of configuration values to Vault."""
|
|
218
|
+
self.set(key, value)
|
|
219
|
+
|
|
182
220
|
def get(self, key, default=None):
|
|
183
221
|
"""Dictionary-style access to configuration values."""
|
|
184
222
|
try:
|
|
@@ -186,6 +224,10 @@ class SectionProxy:
|
|
|
186
224
|
except KeyError:
|
|
187
225
|
return default
|
|
188
226
|
|
|
227
|
+
def set(self, key, value):
|
|
228
|
+
"""Set a configuration value to Vault."""
|
|
229
|
+
self._loader.set(self._section, key, value)
|
|
230
|
+
|
|
189
231
|
def __getattr__(self, key):
|
|
190
232
|
"""Attribute-style access to configuration values."""
|
|
191
233
|
try:
|
|
@@ -193,6 +235,13 @@ class SectionProxy:
|
|
|
193
235
|
except KeyError as e:
|
|
194
236
|
raise AttributeError(str(e))
|
|
195
237
|
|
|
238
|
+
def __setattr__(self, name, value):
|
|
239
|
+
"""Attribute-style setting of configuration values to Vault."""
|
|
240
|
+
if name.startswith("_"):
|
|
241
|
+
super().__setattr__(name, value)
|
|
242
|
+
else:
|
|
243
|
+
self.set(name, value)
|
|
244
|
+
|
|
196
245
|
def __repr__(self):
|
|
197
246
|
"""String representation of the section's configuration."""
|
|
198
247
|
return yaml.dump(self._loader._raw_config[self._section])
|
|
@@ -202,10 +251,40 @@ if __name__ == "__main__":
|
|
|
202
251
|
config_without_vault = ConfigLoader("config.yml")
|
|
203
252
|
print("Without Vault:", config_without_vault.get("jira", "url"))
|
|
204
253
|
|
|
205
|
-
# This will only work if you provide a valid Vault secret ID
|
|
206
|
-
# and hvac package is installed
|
|
207
254
|
config_with_vault = ConfigLoader("config.yml", vault_secret_id=os.getenv("VAULT_SECRET_ID"))
|
|
208
255
|
|
|
209
|
-
|
|
210
|
-
print("With Vault:", config_with_vault
|
|
256
|
+
# 属性方式读取
|
|
257
|
+
print("With Vault (attr):", config_with_vault.test.test)
|
|
258
|
+
|
|
259
|
+
# 字典方式读取
|
|
260
|
+
print("With Vault (dict):", config_with_vault["cis"]["cis_client_id"])
|
|
261
|
+
|
|
262
|
+
# 测试不存在的值
|
|
211
263
|
print("Try val not exists: ", config_with_vault.test.get("not_exists"))
|
|
264
|
+
|
|
265
|
+
# 写入示例 - 属性方式
|
|
266
|
+
try:
|
|
267
|
+
print("Current test value (attr read):", config_with_vault.test.test)
|
|
268
|
+
config_with_vault.test.test = "new_value_123"
|
|
269
|
+
print("After write (attr read):", config_with_vault.test.test)
|
|
270
|
+
except Exception as e:
|
|
271
|
+
print("Attribute write failed:", str(e))
|
|
272
|
+
|
|
273
|
+
# 写入示例 - 字典方式
|
|
274
|
+
try:
|
|
275
|
+
print("Current test value (dict read):", config_with_vault["test"]["test"])
|
|
276
|
+
config_with_vault["test"]["test"] = "new_value_456"
|
|
277
|
+
print("After write (dict read):", config_with_vault["test"]["test"])
|
|
278
|
+
|
|
279
|
+
# 混合方式验证
|
|
280
|
+
print("After write (attr read):", config_with_vault.test.test)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print("Dictionary write failed:", str(e))
|
|
283
|
+
|
|
284
|
+
# 写入新键示例
|
|
285
|
+
try:
|
|
286
|
+
print("Setting new key...")
|
|
287
|
+
config_with_vault["database"]["new_password"] = "secure123!"
|
|
288
|
+
print("New password:", config_with_vault.database.new_password)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print("New key write failed:", str(e))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
xenoslib/__init__.py,sha256=vlMWwdD2gTb9ztpm_orYAm_Y0Be0Es64ifvMQwrE1jU,310
|
|
2
2
|
xenoslib/__main__.py,sha256=UpD3pl4l5ZUFxK6FzNMWRb1AZpEnwerulSFoPb_Pdgw,307
|
|
3
|
-
xenoslib/about.py,sha256=
|
|
4
|
-
xenoslib/base.py,sha256=
|
|
3
|
+
xenoslib/about.py,sha256=JOPWsSbcxv9ls6wxojlF88auWvYf0Ok4vc4yFIolu-Y,228
|
|
4
|
+
xenoslib/base.py,sha256=2qHsptnf_EAaoezr5aQwM6gdPEGnAPn8ommRYtOhD1c,14428
|
|
5
5
|
xenoslib/dev.py,sha256=R6iwKuu-xvaYiBOUyP2gpePyvXss17TOZaCmQEihPCw,1236
|
|
6
6
|
xenoslib/extend.py,sha256=Vl5d8carSGy6ERE2EjTmB9lOo2uIBWs1iz-fKm_TX-M,5947
|
|
7
7
|
xenoslib/linux.py,sha256=EFCQBq_JsmNJc3wGlJlzH9W0tjOA7sSwx0u62R5Ut2k,1318
|
|
@@ -11,9 +11,9 @@ xenoslib/onedrive.py,sha256=-bJJ8Cd_RjJSlDynYqKoZlFKE1HHM34l6NXOQrWOwtg,7783
|
|
|
11
11
|
xenoslib/win_trayicon.py,sha256=7GJwX3c2CS1XWQjsyDK5EfM-MmEHdzPJCTX2sGpWmuU,13115
|
|
12
12
|
xenoslib/windows.py,sha256=lUTD7TowaPqYHgIL6b-GM9PLd-VyJNNGzkzC3K9vOxo,4080
|
|
13
13
|
xenoslib/tools/__init__.py,sha256=D1qP5tD2ZukXHgjKUh0fFl8jUipM3KaPBZBi0vGPypQ,48
|
|
14
|
-
xenoslib/tools/config_loader.py,sha256=
|
|
15
|
-
xenoslib-0.
|
|
16
|
-
xenoslib-0.
|
|
17
|
-
xenoslib-0.
|
|
18
|
-
xenoslib-0.
|
|
19
|
-
xenoslib-0.
|
|
14
|
+
xenoslib/tools/config_loader.py,sha256=dHGqeRX-RvtBCdERsOYTYRIBD8nfYx04XRpps25ob_c,10947
|
|
15
|
+
xenoslib-0.3.0.dist-info/licenses/LICENSE,sha256=lF6hjufhiR-xAje_Wo7ogxV5phz2d5TgkfL5SGshujg,1066
|
|
16
|
+
xenoslib-0.3.0.dist-info/METADATA,sha256=YjQwsxquDj_F_rfl9-GiRVsWRHxmtDE7yh7Q73dKj6g,910
|
|
17
|
+
xenoslib-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
xenoslib-0.3.0.dist-info/top_level.txt,sha256=Rfm4GdW0NyA2AHDNUF2wFQOfAo53mAPibddSHGd3X60,9
|
|
19
|
+
xenoslib-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|