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.
- lean_explore/__init__.py +1 -0
- lean_explore/api/__init__.py +1 -0
- lean_explore/api/client.py +124 -0
- lean_explore/cli/__init__.py +1 -0
- lean_explore/cli/agent.py +781 -0
- lean_explore/cli/config_utils.py +408 -0
- lean_explore/cli/data_commands.py +506 -0
- lean_explore/cli/main.py +659 -0
- lean_explore/defaults.py +117 -0
- lean_explore/local/__init__.py +1 -0
- lean_explore/local/search.py +921 -0
- lean_explore/local/service.py +394 -0
- lean_explore/mcp/__init__.py +1 -0
- lean_explore/mcp/app.py +107 -0
- lean_explore/mcp/server.py +247 -0
- lean_explore/mcp/tools.py +242 -0
- lean_explore/shared/__init__.py +1 -0
- lean_explore/shared/models/__init__.py +1 -0
- lean_explore/shared/models/api.py +117 -0
- lean_explore/shared/models/db.py +411 -0
- lean_explore-0.1.1.dist-info/METADATA +277 -0
- lean_explore-0.1.1.dist-info/RECORD +26 -0
- lean_explore-0.1.1.dist-info/WHEEL +5 -0
- lean_explore-0.1.1.dist-info/entry_points.txt +2 -0
- lean_explore-0.1.1.dist-info/licenses/LICENSE +201 -0
- lean_explore-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -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
|