lean-explore 0.1.1__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.
@@ -0,0 +1,408 @@
1
+ # src/lean_explore/cli/config_utils.py
2
+
3
+ """Utilities for managing CLI user configurations, such as API keys.
4
+
5
+ This module provides functions to save and load user-specific settings,
6
+ such as API keys for Lean Explore and OpenAI, from a configuration
7
+ file stored in the user's home directory. It handles file creation,
8
+ parsing, and sets secure permissions for files containing sensitive
9
+ information.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import pathlib
15
+ from typing import Any, Dict, Optional
16
+
17
+ import toml
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Define the application's configuration directory and file name
22
+ _APP_CONFIG_DIR_NAME: str = "leanexplore"
23
+ _CONFIG_FILENAME: str = "config.toml"
24
+
25
+ # Define keys for Lean Explore API section
26
+ _LEAN_EXPLORE_API_SECTION_NAME: str = "lean_explore_api" # Renamed for clarity
27
+ _LEAN_EXPLORE_API_KEY_NAME: str = "key"
28
+
29
+ # Define keys for OpenAI API section
30
+ _OPENAI_API_SECTION_NAME: str = "openai"
31
+ _OPENAI_API_KEY_NAME: str = "api_key" # Using a distinct key name for clarity
32
+
33
+
34
+ def get_config_file_path() -> pathlib.Path:
35
+ """Constructs and returns the absolute path to the configuration file.
36
+
37
+ The path is typically ~/.config/leanexplore/config.toml.
38
+
39
+ Returns:
40
+ pathlib.Path: The absolute path to the configuration file.
41
+ """
42
+ config_dir = (
43
+ pathlib.Path(os.path.expanduser("~")) / ".config" / _APP_CONFIG_DIR_NAME
44
+ )
45
+ return config_dir / _CONFIG_FILENAME
46
+
47
+
48
+ def _ensure_config_dir_exists() -> None:
49
+ """Ensures that the configuration directory exists.
50
+
51
+ Creates the directory if it's not already present.
52
+
53
+ Raises:
54
+ OSError: If the directory cannot be created due to permission issues
55
+ or other OS-level errors.
56
+ """
57
+ config_file_path = get_config_file_path()
58
+ config_dir = config_file_path.parent
59
+ try:
60
+ config_dir.mkdir(parents=True, exist_ok=True)
61
+ except OSError as e:
62
+ logger.error(f"Failed to create configuration directory {config_dir}: {e}")
63
+ raise
64
+
65
+
66
+ def _load_config_data(config_file_path: pathlib.Path) -> Dict[str, Any]:
67
+ """Loads configuration data from a TOML file.
68
+
69
+ Args:
70
+ config_file_path: Path to the configuration file.
71
+
72
+ Returns:
73
+ A dictionary containing the configuration data. Returns an empty
74
+ dictionary if the file does not exist or is corrupted.
75
+ """
76
+ config_data: Dict[str, Any] = {}
77
+ if config_file_path.exists() and config_file_path.is_file():
78
+ try:
79
+ with open(config_file_path, encoding="utf-8") as f:
80
+ config_data = toml.load(f)
81
+ except toml.TomlDecodeError:
82
+ logger.warning(
83
+ "Configuration file %s is corrupted. Treating as empty.",
84
+ config_file_path,
85
+ )
86
+ # Potentially back up corrupted file before returning empty
87
+ except Exception as e:
88
+ logger.error(
89
+ "Error reading existing config file %s: %s",
90
+ config_file_path,
91
+ e,
92
+ exc_info=True,
93
+ )
94
+ # Decide if to proceed with empty or raise further
95
+ return config_data
96
+
97
+
98
+ def _save_config_data(
99
+ config_file_path: pathlib.Path, config_data: Dict[str, Any]
100
+ ) -> bool:
101
+ """Saves configuration data to a TOML file with secure permissions.
102
+
103
+ Args:
104
+ config_file_path: Path to the configuration file.
105
+ config_data: Dictionary containing the configuration data to save.
106
+
107
+ Returns:
108
+ True if saving was successful, False otherwise.
109
+ """
110
+ try:
111
+ with open(config_file_path, "w", encoding="utf-8") as f:
112
+ toml.dump(config_data, f)
113
+ os.chmod(config_file_path, 0o600) # Set user read/write only
114
+ return True
115
+ except OSError as e:
116
+ logger.error(
117
+ "OS error saving configuration to %s: %s",
118
+ config_file_path,
119
+ e,
120
+ exc_info=True,
121
+ )
122
+ except Exception as e:
123
+ logger.error(
124
+ "Unexpected error saving configuration to %s: %s",
125
+ config_file_path,
126
+ e,
127
+ exc_info=True,
128
+ )
129
+ return False
130
+
131
+
132
+ # --- Lean Explore API Key Management ---
133
+
134
+
135
+ def save_api_key(api_key: str) -> bool:
136
+ """Saves the Lean Explore API key to the user's configuration file.
137
+
138
+ Args:
139
+ api_key: The Lean Explore API key string to save.
140
+
141
+ Returns:
142
+ bool: True if the API key was saved successfully, False otherwise.
143
+ """
144
+ if not api_key or not isinstance(api_key, str):
145
+ logger.error("Attempted to save an invalid or empty Lean Explore API key.")
146
+ return False
147
+
148
+ config_file_path = get_config_file_path()
149
+ try:
150
+ _ensure_config_dir_exists()
151
+ config_data = _load_config_data(config_file_path)
152
+
153
+ if _LEAN_EXPLORE_API_SECTION_NAME not in config_data or not isinstance(
154
+ config_data[_LEAN_EXPLORE_API_SECTION_NAME], dict
155
+ ):
156
+ config_data[_LEAN_EXPLORE_API_SECTION_NAME] = {}
157
+
158
+ config_data[_LEAN_EXPLORE_API_SECTION_NAME][_LEAN_EXPLORE_API_KEY_NAME] = (
159
+ api_key
160
+ )
161
+
162
+ if _save_config_data(config_file_path, config_data):
163
+ logger.info("Lean Explore API key saved to %s", config_file_path)
164
+ return True
165
+ except (
166
+ Exception
167
+ ) as e: # Catch any exception from _ensure_config_dir_exists or broad issues
168
+ logger.error(
169
+ "General error during Lean Explore API key saving process: %s",
170
+ e,
171
+ exc_info=True,
172
+ )
173
+ return False
174
+
175
+
176
+ def load_api_key() -> Optional[str]:
177
+ """Loads the Lean Explore API key from the user's configuration file.
178
+
179
+ Returns:
180
+ Optional[str]: The Lean Explore API key string if found and valid,
181
+ otherwise None.
182
+ """
183
+ config_file_path = get_config_file_path()
184
+ if not config_file_path.exists() or not config_file_path.is_file():
185
+ logger.debug(
186
+ "Configuration file not found at %s for Lean Explore API key.",
187
+ config_file_path,
188
+ )
189
+ return None
190
+
191
+ try:
192
+ config_data = _load_config_data(config_file_path)
193
+ api_key = config_data.get(_LEAN_EXPLORE_API_SECTION_NAME, {}).get(
194
+ _LEAN_EXPLORE_API_KEY_NAME
195
+ )
196
+
197
+ if api_key and isinstance(api_key, str):
198
+ logger.debug(
199
+ "Lean Explore API key loaded successfully from %s", config_file_path
200
+ )
201
+ return api_key
202
+ elif api_key: # Found but not a string
203
+ logger.warning(
204
+ "Lean Explore API key found in %s but is not a valid string.",
205
+ config_file_path,
206
+ )
207
+ else: # Not found under the expected keys
208
+ logger.debug(
209
+ "Lean Explore API key not found under section '%s', key '%s' in %s",
210
+ _LEAN_EXPLORE_API_SECTION_NAME,
211
+ _LEAN_EXPLORE_API_KEY_NAME,
212
+ config_file_path,
213
+ )
214
+ except Exception as e: # Catch any other unexpected errors during loading
215
+ logger.error(
216
+ "Unexpected error loading Lean Explore API key from %s: %s",
217
+ config_file_path,
218
+ e,
219
+ exc_info=True,
220
+ )
221
+ return None
222
+
223
+
224
+ def delete_api_key() -> bool:
225
+ """Deletes the Lean Explore API key from the user's configuration file.
226
+
227
+ Returns:
228
+ bool: True if the API key was successfully removed or if it did not exist;
229
+ False if an error occurred.
230
+ """
231
+ config_file_path = get_config_file_path()
232
+ if not config_file_path.exists():
233
+ logger.info(
234
+ "No Lean Explore API key to delete: configuration file does not exist."
235
+ )
236
+ return True
237
+
238
+ try:
239
+ config_data = _load_config_data(config_file_path)
240
+ api_section = config_data.get(_LEAN_EXPLORE_API_SECTION_NAME)
241
+
242
+ if (
243
+ api_section
244
+ and isinstance(api_section, dict)
245
+ and _LEAN_EXPLORE_API_KEY_NAME in api_section
246
+ ):
247
+ del api_section[_LEAN_EXPLORE_API_KEY_NAME]
248
+ logger.info("Lean Explore API key removed from configuration data.")
249
+
250
+ if not api_section: # If the section is now empty
251
+ del config_data[_LEAN_EXPLORE_API_SECTION_NAME]
252
+ logger.info(
253
+ "Empty '%s' section removed.", _LEAN_EXPLORE_API_SECTION_NAME
254
+ )
255
+
256
+ if _save_config_data(config_file_path, config_data):
257
+ logger.info("Lean Explore API key deleted from %s", config_file_path)
258
+ return True
259
+ return False # Save failed
260
+ else:
261
+ logger.info(
262
+ "Lean Explore API key not found in %s, no deletion performed.",
263
+ config_file_path,
264
+ )
265
+ return True # Key wasn't there, so considered successful
266
+
267
+ except Exception as e:
268
+ logger.error(
269
+ "Unexpected error deleting Lean Explore API key from %s: %s",
270
+ config_file_path,
271
+ e,
272
+ exc_info=True,
273
+ )
274
+ return False
275
+
276
+
277
+ # --- OpenAI API Key Management ---
278
+
279
+
280
+ def save_openai_api_key(api_key: str) -> bool:
281
+ """Saves the OpenAI API key to the user's configuration file.
282
+
283
+ The API key is stored in the same TOML formatted file as other configurations,
284
+ under a distinct section. File permissions are set securely.
285
+
286
+ Args:
287
+ api_key: The OpenAI API key string to save.
288
+
289
+ Returns:
290
+ bool: True if the API key was saved successfully, False otherwise.
291
+ """
292
+ if not api_key or not isinstance(api_key, str):
293
+ logger.error("Attempted to save an invalid or empty OpenAI API key.")
294
+ return False
295
+
296
+ config_file_path = get_config_file_path()
297
+ try:
298
+ _ensure_config_dir_exists()
299
+ config_data = _load_config_data(config_file_path)
300
+
301
+ if _OPENAI_API_SECTION_NAME not in config_data or not isinstance(
302
+ config_data[_OPENAI_API_SECTION_NAME], dict
303
+ ):
304
+ config_data[_OPENAI_API_SECTION_NAME] = {}
305
+
306
+ config_data[_OPENAI_API_SECTION_NAME][_OPENAI_API_KEY_NAME] = api_key
307
+
308
+ if _save_config_data(config_file_path, config_data):
309
+ logger.info("OpenAI API key saved to %s", config_file_path)
310
+ return True
311
+ except Exception as e:
312
+ logger.error(
313
+ "General error during OpenAI API key saving process: %s", e, exc_info=True
314
+ )
315
+ return False
316
+
317
+
318
+ def load_openai_api_key() -> Optional[str]:
319
+ """Loads the OpenAI API key from the user's configuration file.
320
+
321
+ Returns:
322
+ Optional[str]: The OpenAI API key string if found and valid, otherwise None.
323
+ """
324
+ config_file_path = get_config_file_path()
325
+ if not config_file_path.exists() or not config_file_path.is_file():
326
+ logger.debug(
327
+ "Configuration file not found at %s for OpenAI API key.", config_file_path
328
+ )
329
+ return None
330
+
331
+ try:
332
+ config_data = _load_config_data(config_file_path)
333
+ api_key = config_data.get(_OPENAI_API_SECTION_NAME, {}).get(
334
+ _OPENAI_API_KEY_NAME
335
+ )
336
+
337
+ if api_key and isinstance(api_key, str):
338
+ logger.debug("OpenAI API key loaded successfully from %s", config_file_path)
339
+ return api_key
340
+ elif api_key: # Found but not a string
341
+ logger.warning(
342
+ "OpenAI API key found in %s but is not a valid string.",
343
+ config_file_path,
344
+ )
345
+ else: # Not found under the expected keys
346
+ logger.debug(
347
+ "OpenAI API key not found under section '%s', key '%s' in %s",
348
+ _OPENAI_API_SECTION_NAME,
349
+ _OPENAI_API_KEY_NAME,
350
+ config_file_path,
351
+ )
352
+ except Exception as e:
353
+ logger.error(
354
+ "Unexpected error loading OpenAI API key from %s: %s",
355
+ config_file_path,
356
+ e,
357
+ exc_info=True,
358
+ )
359
+ return None
360
+
361
+
362
+ def delete_openai_api_key() -> bool:
363
+ """Deletes the OpenAI API key from the user's configuration file.
364
+
365
+ Returns:
366
+ bool: True if the API key was successfully removed or if it did not exist;
367
+ False if an error occurred.
368
+ """
369
+ config_file_path = get_config_file_path()
370
+ if not config_file_path.exists():
371
+ logger.info("No OpenAI API key to delete: configuration file does not exist.")
372
+ return True
373
+
374
+ try:
375
+ config_data = _load_config_data(config_file_path)
376
+ api_section = config_data.get(_OPENAI_API_SECTION_NAME)
377
+
378
+ if (
379
+ api_section
380
+ and isinstance(api_section, dict)
381
+ and _OPENAI_API_KEY_NAME in api_section
382
+ ):
383
+ del api_section[_OPENAI_API_KEY_NAME]
384
+ logger.info("OpenAI API key removed from configuration data.")
385
+
386
+ if not api_section: # If the section is now empty
387
+ del config_data[_OPENAI_API_SECTION_NAME]
388
+ logger.info("Empty '%s' section removed.", _OPENAI_API_SECTION_NAME)
389
+
390
+ if _save_config_data(config_file_path, config_data):
391
+ logger.info("OpenAI API key deleted from %s", config_file_path)
392
+ return True
393
+ return False # Save failed
394
+ else:
395
+ logger.info(
396
+ "OpenAI API key not found in %s, no deletion performed.",
397
+ config_file_path,
398
+ )
399
+ return True # Key wasn't there, so considered successful
400
+
401
+ except Exception as e:
402
+ logger.error(
403
+ "Unexpected error deleting OpenAI API key from %s: %s",
404
+ config_file_path,
405
+ e,
406
+ exc_info=True,
407
+ )
408
+ return False