From 7668b0f3eea995be563c7c6623025f90880ae8ce Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 13:48:16 +0530 Subject: [PATCH 1/8] fix: update coverage timestamp in coverage.xml --- coverage.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage.xml b/coverage.xml index 056bb8b..5a1f8d8 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + From 486db57560103c3553686c84aabb354b10b6194a Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 15:36:19 +0530 Subject: [PATCH 2/8] feat: add dotenv support for loading environment variables from .env files --- .coverage | Bin 53248 -> 53248 bytes coverage.xml | 291 ++++++++++++++---------- penify_hook/commands/config_commands.py | 166 ++++++++++++-- requirements.txt | 1 + 4 files changed, 317 insertions(+), 141 deletions(-) diff --git a/.coverage b/.coverage index 8686e8f4121e692e578f423a1a21233b0c03b67e..13d9ed9f37712ea5a8485d8748d44eb203400530 100644 GIT binary patch delta 111 zcmZozz}&Eac>`O6KokT2PyUbmclnR;H}U83M{O1qaN(bv-LK{*!{Nlr%E;Ns!urp? qouTQ^0|zDs1_nk4AkhHC42&Q`07O8TU=~yk%4T3-+I+0v$^iftF%^aY delta 94 zcmZozz}&Eac>`O6KsE#aPyUbmclnR;Pv)=Z&)zI35XwI}s$b2Nfy0oMg^@Fb^`Acj a1A_t-I{;Y%KsFq(z*#^B$L3@GRt^A6^bxuM diff --git a/coverage.xml b/coverage.xml index 5a1f8d8..14aa7a9 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,12 +1,12 @@ - + /Users/sumansaurabh/Documents/my-startup/penify-cli/penify_hook - + @@ -840,7 +840,7 @@ - + @@ -884,9 +884,9 @@ - - - + + + @@ -901,7 +901,7 @@ - + @@ -1051,7 +1051,7 @@ - + @@ -1065,219 +1065,262 @@ - + + + + + + - - - - - - - + + - - - - + + + + + + + + + + - - - - + + + - - - - + + - - - + + + - - - - - + + - - - - - - - - + + - - - + - - - - - + + - - - + + + - + + - - - - + + + + - - - - - - + - - - - - - - + - - + + + + + - - - - - - - - - - - - - - - - - - + + + - - + + + + - - + + - - - - + + - + + + + + - - + - - - - - - - - + + + + + + + - + - - + - - - + - + - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 4d27c42..08b82f9 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -8,8 +8,59 @@ from pathlib import Path from threading import Thread import logging +import sys +from typing import Dict, Any, Optional, Union from penify_hook.utils import recursive_search_git_folder +# Try to import dotenv, but don't fail if it's not available +try: + from dotenv import load_dotenv + DOTENV_AVAILABLE = True +except ImportError: + DOTENV_AVAILABLE = False + + +def load_env_files() -> None: + """ + Load environment variables from .env files in various locations: + 1. Current directory + 2. Git repo root directory + 3. User home directory + + This function is called when the module is imported, ensuring env variables + are available throughout the application lifecycle. + """ + if not DOTENV_AVAILABLE: + logging.warning("python-dotenv is not installed. .env file loading is disabled.") + logging.warning("Run 'pip install python-dotenv' to enable .env file support.") + return + + # Load from current directory + load_dotenv(override=True) + + # Load from Git repo root + try: + current_dir = os.getcwd() + repo_root = recursive_search_git_folder(current_dir) + if repo_root: + repo_env = Path(repo_root) / '.env' + if repo_env.exists(): + load_dotenv(dotenv_path=repo_env, override=True) + except Exception as e: + logging.warning(f"Failed to load .env from Git repo: {str(e)}") + + # Load from user home directory + try: + home_env = Path.home() / '.env' + if home_env.exists(): + load_dotenv(dotenv_path=home_env, override=True) + except Exception as e: + logging.warning(f"Failed to load .env from home directory: {str(e)}") + + +# Load environment variables when module is imported +load_env_files() + def get_penify_config() -> Path: """ @@ -40,7 +91,21 @@ def get_penify_config() -> Path: with open(penify_dir / 'config.json', 'w') as f: json.dump({}, f) return penify_dir / 'config.json' + + +def get_env_var_or_default(env_var: str, default: Any = None) -> Any: + """ + Get environment variable or return default value. + Args: + env_var: The environment variable name + default: Default value if environment variable is not set + + Returns: + Value of the environment variable or default + """ + return os.environ.get(env_var, default) + def save_llm_config(model, api_base, api_key): """ @@ -74,14 +139,14 @@ def save_llm_config(model, api_base, api_key): print(f"Error saving LLM configuration: {str(e)}") return False + def save_jira_config(url, username, api_token): """ Save JIRA configuration settings in the .penify file. """ from penify_hook.utils import recursive_search_git_folder - home_dir = Path.home() - penify_file = home_dir / '.penify' + penify_file = get_penify_config() config = {} if penify_file.exists(): @@ -107,35 +172,94 @@ def save_jira_config(url, username, api_token): print(f"Error saving JIRA configuration: {str(e)}") return False -def get_llm_config(): + +def get_llm_config() -> Dict[str, str]: """ - Get LLM configuration from the .penify file. + Get LLM configuration with environment variables having highest priority. + + Environment variables: + - PENIFY_LLM_MODEL: Model name + - PENIFY_LLM_API_BASE: API base URL + - PENIFY_LLM_API_KEY: API key + + Returns: + dict: Configuration dictionary with model, api_base, and api_key """ + # Initialize with environment variables + config = { + 'model': get_env_var_or_default('PENIFY_LLM_MODEL'), + 'api_base': get_env_var_or_default('PENIFY_LLM_API_BASE'), + 'api_key': get_env_var_or_default('PENIFY_LLM_API_KEY') + } + + # Remove None values + config = {k: v for k, v in config.items() if v is not None} + + # If we have all config from environment variables, return early + if all(k in config for k in ['model', 'api_base', 'api_key']): + return config + + # Otherwise load from config file and merge config_file = get_penify_config() if config_file.exists(): try: with open(config_file, 'r') as f: - config = json.load(f) - return config.get('llm', {}) + file_config = json.load(f) + file_llm_config = file_config.get('llm', {}) + + # Only override values not set by environment variables + for k, v in file_llm_config.items(): + if k not in config: + config[k] = v except (json.JSONDecodeError, Exception) as e: print(f"Error reading .penify config file: {str(e)}") - return {} + return config -def get_jira_config(): + +def get_jira_config() -> Dict[str, str]: """ - Get JIRA configuration from the .penify file. + Get JIRA configuration with environment variables having highest priority. + + Environment variables: + - PENIFY_JIRA_URL: JIRA URL + - PENIFY_JIRA_USER: JIRA username + - PENIFY_JIRA_TOKEN: JIRA API token + + Returns: + dict: Configuration dictionary with url, username, and api_token """ + # Initialize with environment variables + config = { + 'url': get_env_var_or_default('PENIFY_JIRA_URL'), + 'username': get_env_var_or_default('PENIFY_JIRA_USER'), + 'api_token': get_env_var_or_default('PENIFY_JIRA_TOKEN') + } + + # Remove None values + config = {k: v for k, v in config.items() if v is not None} + + # If we have all config from environment variables, return early + if all(k in config for k in ['url', 'username', 'api_token']): + return config + + # Otherwise load from config file and merge config_file = get_penify_config() if config_file.exists(): try: with open(config_file, 'r') as f: - config = json.load(f) - return config.get('jira', {}) + file_config = json.load(f) + file_jira_config = file_config.get('jira', {}) + + # Only override values not set by environment variables + for k, v in file_jira_config.items(): + if k not in config: + config[k] = v except (json.JSONDecodeError, Exception) as e: print(f"Error reading .penify config file: {str(e)}") - return {} + return config + def config_llm_web(): """ @@ -239,6 +363,7 @@ def log_message(self, format, *args): print("Configuration completed.") + def config_jira_web(): """ Open a web browser interface for configuring JIRA settings. @@ -344,16 +469,23 @@ def log_message(self, format, *args): print("Configuration completed.") -def get_token(): + +def get_token() -> Optional[str]: """ - Get the token based on priority. + Get the API token based on priority: + 1. Environment variable PENIFY_API_TOKEN + 2. Config file 'api_keys' value + + Returns: + str or None: API token if found, None otherwise """ - import os - env_token = os.getenv('PENIFY_API_TOKEN') + # Check environment variable first + env_token = get_env_var_or_default('PENIFY_API_TOKEN') if env_token: return env_token - config_file = Path.home() / '.penify' + # Check config file + config_file = get_penify_config() if config_file.exists(): try: with open(config_file, 'r') as f: diff --git a/requirements.txt b/requirements.txt index 0679314..216bc40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ requests>=2.25.0 gitpython>=3.1.0 tqdm>=4.62.0 +python-dotenv>=1.0.0 # Testing requirements pytest>=7.0.0 From 5bb62bda231d40abc6d04b7c77f75001f955beed Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 15:51:44 +0530 Subject: [PATCH 3/8] feat: add Pipfile for dependency management and update coverage.xml timestamps --- .coverage | Bin 53248 -> 53248 bytes Pipfile | 19 ++ coverage.xml | 219 ++++++++++++------------ penify_hook/commands/config_commands.py | 3 +- penify_hook/config_command.py | 28 +-- 5 files changed, 148 insertions(+), 121 deletions(-) create mode 100644 Pipfile diff --git a/.coverage b/.coverage index 13d9ed9f37712ea5a8485d8748d44eb203400530..7cd7a11bdeacfd45196a7c117278de50b68546cd 100644 GIT binary patch delta 54 zcmZozz}&Eac|&)<(tKwI$A1qTm>3usm>Pft10#q9Vkm$JAakOD1S8AlWBpbR0C_PB AeEi2wiq diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..8e6917b --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = ">=2.25.0" +gitpython = ">=3.1.0" +tqdm = ">=4.62.0" +python-dotenv = ">=1.0.0" +pytest = ">=7.0.0" +pytest-cov = ">=4.0.0" +coverage = ">=6.0.0" +coverage-badge = ">=1.1.0" + +[dev-packages] + +[requires] +python_version = "3.13" diff --git a/coverage.xml b/coverage.xml index 14aa7a9..3db86d7 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,12 +1,12 @@ - + /Users/sumansaurabh/Documents/my-startup/penify-cli/penify_hook - + @@ -195,29 +195,31 @@ - - - + - + + + - - - - + + + - - + + + - + + + @@ -901,7 +903,7 @@ - + @@ -1051,7 +1053,7 @@ - + @@ -1066,18 +1068,18 @@ - + - + - - - + + + - - + + @@ -1097,30 +1099,30 @@ - + - - + + - - - + + + - - - - - + + + + + - - + + @@ -1128,18 +1130,18 @@ - - - - + + + + - - + + @@ -1147,172 +1149,172 @@ - - - - + + + + - + - + - - - - - + + + + + - + - + - - - + + + - - + + - - + + - + - - + + - - - + + + - + - + - + - + - - + + - + - + - - - + + + - - - + + + - - + + - - + + - + - - + + - - - + + + - + - + - - - + + + - - + + - + - + - - - + + + - - - + + + - + @@ -1320,7 +1322,8 @@ - + + diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 08b82f9..c0d72ac 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -10,7 +10,6 @@ import logging import sys from typing import Dict, Any, Optional, Union -from penify_hook.utils import recursive_search_git_folder # Try to import dotenv, but don't fail if it's not available try: @@ -40,6 +39,7 @@ def load_env_files() -> None: # Load from Git repo root try: + from penify_hook.utils import recursive_search_git_folder current_dir = os.getcwd() repo_root = recursive_search_git_folder(current_dir) if repo_root: @@ -69,6 +69,7 @@ def get_penify_config() -> Path: and its parent directories until it finds it or reaches the home directory. """ current_dir = os.getcwd() + from penify_hook.utils import recursive_search_git_folder home_dir = recursive_search_git_folder(current_dir) diff --git a/penify_hook/config_command.py b/penify_hook/config_command.py index 6f76894..8ee911f 100644 --- a/penify_hook/config_command.py +++ b/penify_hook/config_command.py @@ -6,42 +6,45 @@ def setup_config_parser(parent_parser): parser = parent_parser.add_subparsers(title="config_type", dest="config_type") # Config subcommand: llm - llm_config_parser = parser.add_parser("llm", help="Configure LLM settings.") + llm_config_parser = parser.add_parser("llm-cmd", help="Configure LLM settings.") llm_config_parser.add_argument("--model", required=True, help="LLM model to use") llm_config_parser.add_argument("--api-base", help="API base URL for the LLM service") llm_config_parser.add_argument("--api-key", help="API key for the LLM service") # Config subcommand: llm-web - parser.add_parser("llm-web", help="Configure LLM settings through a web interface") + parser.add_parser("llm", help="Configure LLM settings through a web interface") # Config subcommand: jira - jira_config_parser = parser.add_parser("jira", help="Configure JIRA settings.") + jira_config_parser = parser.add_parser("jira-cmd", help="Configure JIRA settings.") jira_config_parser.add_argument("--url", required=True, help="JIRA base URL") jira_config_parser.add_argument("--username", required=True, help="JIRA username or email") jira_config_parser.add_argument("--api-token", required=True, help="JIRA API token") jira_config_parser.add_argument("--verify", action="store_true", help="Verify JIRA connection") # Config subcommand: jira-web - parser.add_parser("jira-web", help="Configure JIRA settings through a web interface") + parser.add_parser("jira", help="Configure JIRA settings through a web interface") # Add all other necessary arguments for config command def handle_config(args): # Only import dependencies needed for config functionality here - from penify_hook.commands.config_commands import save_llm_config - from penify_hook.jira_client import JiraClient # Import moved here - from penify_hook.commands.config_commands import config_jira_web, config_llm_web, save_jira_config + + - if args.config_type == "llm": + if args.config_type == "llm-cmd": + from penify_hook.commands.config_commands import save_llm_config save_llm_config(args.model, args.api_base, args.api_key) print(f"LLM configuration set: Model={args.model}, API Base={args.api_base or 'default'}") - elif args.config_type == "llm-web": + elif args.config_type == "llm": + from penify_hook.commands.config_commands import config_llm_web config_llm_web() - elif args.config_type == "jira": + elif args.config_type == "jira-cmd": + from penify_hook.commands.config_commands import save_jira_config save_jira_config(args.url, args.username, args.api_token) print(f"JIRA configuration set: URL={args.url}, Username={args.username}") + from penify_hook.jira_client import JiraClient # Import moved here # Verify connection if requested if args.verify: @@ -58,11 +61,12 @@ def handle_config(args): else: print("JIRA package not installed. Cannot verify connection.") - elif args.config_type == "jira-web": + elif args.config_type == "jira": + from penify_hook.commands.config_commands import config_jira_web config_jira_web() else: - print("Please specify a config type: llm, llm-web, jira, or jira-web") + print("Please specify a config type: llm, jira") return 1 return 0 From ec55675cf71462df5723c337e56dda069e8f5a7e Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 16:00:41 +0530 Subject: [PATCH 4/8] feat: add .env file for environment variable management and update load_env_files function --- .coverage | Bin 53248 -> 53248 bytes .env | 3 + coverage.xml | 297 +++++++++++++----------- penify_hook/commands/config_commands.py | 243 ++++++++++--------- 4 files changed, 293 insertions(+), 250 deletions(-) create mode 100644 .env diff --git a/.coverage b/.coverage index 7cd7a11bdeacfd45196a7c117278de50b68546cd..b99d286ea3e709549c654929edc976327488ab21 100644 GIT binary patch delta 112 zcmZozz}&Eac>`O6KsW>cPyUbmclnR;*Yjudhi?`XaO9sH)vw|1%F4>f*~rEEZ@x1_ pQ?tE<5Ca2)00Rq<1T(;t1CZnZVn#3zLNp+;85o#0AM3Yr007826Vd`O6KokT2PyUbmclnR;H}U83M{O1qaN(aE-mjtU#LCLZ*~r5BZ@x2w k - + @@ -903,7 +903,7 @@ - + @@ -1053,7 +1053,7 @@ - + @@ -1074,256 +1074,273 @@ - - + - + + + - - - - - + + + - - - + + + + + - - + + - - - + + + - - - + + + - - + - + + - - + - - - - - - - - - + + + + + + - - + + + + + + + - - - - + + + + + + + - - - + + + + + - - - - - - + + + + + + + + - + + + + + + + + + - - - + - - - - + + + + + + + + + + - - - - - - - + + - - - - - - - - + - - + - - - - - - - - - + + + - + + - + + + + - + + - - - + - - - - - - + + + + - - + - + - - - - - - + + + + + + + + - + - - + + - - - - - + + + - + + - + + + + - + + - - - + - - - - - + + + + - + - - - - + + - - - - - + + + + + + + + + + + - + - - - - + + + + + + + + + + + + + diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index c0d72ac..77e11ef 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -21,10 +21,11 @@ def load_env_files() -> None: """ - Load environment variables from .env files in various locations: - 1. Current directory - 2. Git repo root directory - 3. User home directory + Load environment variables from .env files in various locations, + with proper priority (later files override earlier ones): + 1. User home directory .env (lowest priority) + 2. Git repo root directory .env (if in a git repo) + 3. Current directory .env (highest priority) This function is called when the module is imported, ensuring env variables are available throughout the application lifecycle. @@ -34,28 +35,30 @@ def load_env_files() -> None: logging.warning("Run 'pip install python-dotenv' to enable .env file support.") return - # Load from current directory - load_dotenv(override=True) + # Load from user home directory (lowest priority) + try: + home_env = Path.home() / '.env' + if home_env.exists(): + load_dotenv(dotenv_path=home_env, override=False) + except Exception as e: + logging.warning(f"Failed to load .env from home directory: {str(e)}") - # Load from Git repo root + # Load from Git repo root (medium priority) try: from penify_hook.utils import recursive_search_git_folder current_dir = os.getcwd() repo_root = recursive_search_git_folder(current_dir) - if repo_root: + if repo_root and repo_root != str(Path.home()): repo_env = Path(repo_root) / '.env' - if repo_env.exists(): + if repo_env.exists() and repo_env != home_env: load_dotenv(dotenv_path=repo_env, override=True) except Exception as e: logging.warning(f"Failed to load .env from Git repo: {str(e)}") - # Load from user home directory - try: - home_env = Path.home() / '.env' - if home_env.exists(): - load_dotenv(dotenv_path=home_env, override=True) - except Exception as e: - logging.warning(f"Failed to load .env from home directory: {str(e)}") + # Load from current directory (highest priority) + current_env = Path(os.getcwd()) / '.env' + if current_env.exists() and (not repo_root or current_env != Path(repo_root) / '.env'): + load_dotenv(dotenv_path=current_env, override=True) # Load environment variables when module is imported @@ -110,31 +113,56 @@ def get_env_var_or_default(env_var: str, default: Any = None) -> Any: def save_llm_config(model, api_base, api_key): """ - Save LLM configuration settings in the .penify file. + Save LLM configuration settings to .env file. + + This function saves LLM configuration in the following priority: + 1. Git repo root .env (if inside a git repo) + 2. User home directory .env """ - - penify_file = get_penify_config() + from pathlib import Path + import os - config = {} - if penify_file.exists(): - try: - with open(penify_file, 'r') as f: - config = json.load(f) - except (json.JSONDecodeError, IOError, OSError) as e: - print(f"Error reading configuration file: {str(e)}") - # Continue with empty config - - # Update or add LLM configuration - config['llm'] = { - 'model': model, - 'api_base': api_base, - 'api_key': api_key - } + if not DOTENV_AVAILABLE: + print("python-dotenv is not installed. Run 'pip install python-dotenv' to enable .env file support.") + return False + # Try to find Git repo root + try: + from penify_hook.utils import recursive_search_git_folder + current_dir = os.getcwd() + repo_root = recursive_search_git_folder(current_dir) + env_file = Path(repo_root) / '.env' if repo_root else Path.home() / '.env' + except Exception as e: + print(f"Failed to determine Git repo root: {str(e)}") + env_file = Path.home() / '.env' + + # Read existing .env content + env_content = {} + if env_file.exists(): + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_content[key.strip()] = value.strip() + + # Update LLM configuration + env_content['PENIFY_LLM_MODEL'] = model + env_content['PENIFY_LLM_API_BASE'] = api_base + env_content['PENIFY_LLM_API_KEY'] = api_key + + # Write back to .env file try: - with open(penify_file, 'w') as f: - json.dump(config, f) - print(f"LLM configuration saved to {penify_file}") + with open(env_file, 'w') as f: + for key, value in env_content.items(): + f.write(f"{key}={value}\n") + print(f"LLM configuration saved to {env_file}") + + # Reload environment variables to make changes immediately available + if DOTENV_AVAILABLE: + from dotenv import load_dotenv + load_dotenv(dotenv_path=env_file, override=True) + return True except Exception as e: print(f"Error saving LLM configuration: {str(e)}") @@ -143,31 +171,56 @@ def save_llm_config(model, api_base, api_key): def save_jira_config(url, username, api_token): """ - Save JIRA configuration settings in the .penify file. + Save JIRA configuration settings to .env file. + + This function saves JIRA configuration in the following priority: + 1. Git repo root .env (if inside a git repo) + 2. User home directory .env """ - from penify_hook.utils import recursive_search_git_folder - - penify_file = get_penify_config() + from pathlib import Path + import os - config = {} - if penify_file.exists(): - try: - with open(penify_file, 'r') as f: - config = json.load(f) - except json.JSONDecodeError: - pass - - # Update or add JIRA configuration - config['jira'] = { - 'url': url, - 'username': username, - 'api_token': api_token - } + if not DOTENV_AVAILABLE: + print("python-dotenv is not installed. Run 'pip install python-dotenv' to enable .env file support.") + return False + # Try to find Git repo root + try: + from penify_hook.utils import recursive_search_git_folder + current_dir = os.getcwd() + repo_root = recursive_search_git_folder(current_dir) + env_file = Path(repo_root) / '.env' if repo_root else Path.home() / '.env' + except Exception as e: + print(f"Failed to determine Git repo root: {str(e)}") + env_file = Path.home() / '.env' + + # Read existing .env content + env_content = {} + if env_file.exists(): + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_content[key.strip()] = value.strip() + + # Update JIRA configuration + env_content['PENIFY_JIRA_URL'] = url + env_content['PENIFY_JIRA_USER'] = username + env_content['PENIFY_JIRA_TOKEN'] = api_token + + # Write back to .env file try: - with open(penify_file, 'w') as f: - json.dump(config, f) - print(f"JIRA configuration saved to {penify_file}") + with open(env_file, 'w') as f: + for key, value in env_content.items(): + f.write(f"{key}={value}\n") + print(f"JIRA configuration saved to {env_file}") + + # Reload environment variables to make changes immediately available + if DOTENV_AVAILABLE: + from dotenv import load_dotenv + load_dotenv(dotenv_path=env_file, override=True) + return True except Exception as e: print(f"Error saving JIRA configuration: {str(e)}") @@ -176,7 +229,7 @@ def save_jira_config(url, username, api_token): def get_llm_config() -> Dict[str, str]: """ - Get LLM configuration with environment variables having highest priority. + Get LLM configuration from environment variables. Environment variables: - PENIFY_LLM_MODEL: Model name @@ -186,41 +239,26 @@ def get_llm_config() -> Dict[str, str]: Returns: dict: Configuration dictionary with model, api_base, and api_key """ - # Initialize with environment variables + # Ensure environment variables are loaded + if DOTENV_AVAILABLE: + load_env_files() + + # Get values from environment variables config = { - 'model': get_env_var_or_default('PENIFY_LLM_MODEL'), - 'api_base': get_env_var_or_default('PENIFY_LLM_API_BASE'), - 'api_key': get_env_var_or_default('PENIFY_LLM_API_KEY') + 'model': get_env_var_or_default('PENIFY_LLM_MODEL', ''), + 'api_base': get_env_var_or_default('PENIFY_LLM_API_BASE', ''), + 'api_key': get_env_var_or_default('PENIFY_LLM_API_KEY', '') } - # Remove None values - config = {k: v for k, v in config.items() if v is not None} - - # If we have all config from environment variables, return early - if all(k in config for k in ['model', 'api_base', 'api_key']): - return config - - # Otherwise load from config file and merge - config_file = get_penify_config() - if config_file.exists(): - try: - with open(config_file, 'r') as f: - file_config = json.load(f) - file_llm_config = file_config.get('llm', {}) - - # Only override values not set by environment variables - for k, v in file_llm_config.items(): - if k not in config: - config[k] = v - except (json.JSONDecodeError, Exception) as e: - print(f"Error reading .penify config file: {str(e)}") + # Remove empty values + config = {k: v for k, v in config.items() if v} return config def get_jira_config() -> Dict[str, str]: """ - Get JIRA configuration with environment variables having highest priority. + Get JIRA configuration from environment variables. Environment variables: - PENIFY_JIRA_URL: JIRA URL @@ -230,34 +268,19 @@ def get_jira_config() -> Dict[str, str]: Returns: dict: Configuration dictionary with url, username, and api_token """ - # Initialize with environment variables + # Ensure environment variables are loaded + if DOTENV_AVAILABLE: + load_env_files() + + # Get values from environment variables config = { - 'url': get_env_var_or_default('PENIFY_JIRA_URL'), - 'username': get_env_var_or_default('PENIFY_JIRA_USER'), - 'api_token': get_env_var_or_default('PENIFY_JIRA_TOKEN') + 'url': get_env_var_or_default('PENIFY_JIRA_URL', ''), + 'username': get_env_var_or_default('PENIFY_JIRA_USER', ''), + 'api_token': get_env_var_or_default('PENIFY_JIRA_TOKEN', '') } - # Remove None values - config = {k: v for k, v in config.items() if v is not None} - - # If we have all config from environment variables, return early - if all(k in config for k in ['url', 'username', 'api_token']): - return config - - # Otherwise load from config file and merge - config_file = get_penify_config() - if config_file.exists(): - try: - with open(config_file, 'r') as f: - file_config = json.load(f) - file_jira_config = file_config.get('jira', {}) - - # Only override values not set by environment variables - for k, v in file_jira_config.items(): - if k not in config: - config[k] = v - except (json.JSONDecodeError, Exception) as e: - print(f"Error reading .penify config file: {str(e)}") + # Remove empty values + config = {k: v for k, v in config.items() if v} return config From 3b5339d8e0bedeef966b05029334b05e3d72272b Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 16:12:22 +0530 Subject: [PATCH 5/8] feat: enhance credential management by saving API tokens to .env file and updating fallback logic --- .coverage | Bin 53248 -> 53248 bytes .gitignore | 7 ++ coverage.xml | 89 ++++++++++++++++-------- penify_hook/commands/auth_commands.py | 54 ++++++++++++-- penify_hook/commands/config_commands.py | 6 +- 5 files changed, 121 insertions(+), 35 deletions(-) diff --git a/.coverage b/.coverage index b99d286ea3e709549c654929edc976327488ab21..2b4ac98790af720935cd5e818d58e716ff71c916 100644 GIT binary patch delta 16 XcmZozz}&Eac|&hMWBum7{vrneJCO$! delta 16 XcmZozz}&Eac|&hMqy6T-{vrneIuQod diff --git a/.gitignore b/.gitignore index e095838..6dffe7a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ __pycache__/ build/ .penify/ .penify/* +.env/ +*.env +*.DS_Store +*.log +*.sqlite3 +*.db +.env \ No newline at end of file diff --git a/coverage.xml b/coverage.xml index 26a6c3b..e7e3625 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + @@ -903,7 +903,7 @@ - + @@ -922,59 +922,86 @@ - - - - - - + + + - - + + + + - - + - - + - - + + + + + + + + + - - - + - + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -1053,7 +1080,7 @@ - + @@ -1091,7 +1118,7 @@ - + @@ -1331,16 +1358,18 @@ - + - - + + + + diff --git a/penify_hook/commands/auth_commands.py b/penify_hook/commands/auth_commands.py index 8c41041..40f0f8b 100644 --- a/penify_hook/commands/auth_commands.py +++ b/penify_hook/commands/auth_commands.py @@ -4,19 +4,63 @@ import socketserver import urllib.parse import random +import os from threading import Thread from pathlib import Path -from ..api_client import APIClient def save_credentials(api_key): """ - Save the token and API keys in the .penify file in the user's home directory. + Save the token and API keys based on priority: + 1. .env file in Git repo root (if in a git repo) + 2. .penify file in home directory (global fallback) + + Args: + api_key: The API key to save + + Returns: + bool: True if saved successfully, False otherwise """ + # Try to save in .env file in git repo first + try: + from ..utils import recursive_search_git_folder + current_dir = os.getcwd() + repo_root = recursive_search_git_folder(current_dir) + + if repo_root: + # We're in a git repo, save to .env file + env_file = Path(repo_root) / '.env' + try: + # Read existing .env content + env_content = {} + if env_file.exists(): + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_content[key.strip()] = value.strip() + + # Update API token + env_content['PENIFY_API_TOKEN'] = api_key + + # Write back to .env file + with open(env_file, 'w') as f: + for key, value in env_content.items(): + f.write(f"{key}={value}\n") + + print(f"API token saved to {env_file}") + return True + except Exception as e: + print(f"Error saving to .env file: {str(e)}") + # Fall back to saving in .penify global config + except Exception as e: + print(f"Error finding git repository: {str(e)}") + + # Fall back to global .penify file in home directory home_dir = Path.home() penify_file = home_dir / '.penify' - - # if the file already exists, add the new api key to the existing file + # If the file already exists, add the new api key to the existing file if penify_file.exists(): with open(penify_file, 'r') as f: credentials = json.load(f) @@ -29,6 +73,7 @@ def save_credentials(api_key): try: with open(penify_file, 'w') as f: json.dump(credentials, f) + print(f"API token saved to global config {penify_file}") return True except Exception as e: print(f"Error saving credentials: {str(e)}") @@ -74,6 +119,7 @@ def do_GET(self): self.wfile.write(response.encode()) print(f"\nLogin successful! Fetching API keys...") + from ..api_client import APIClient api_key = APIClient(api_url, None, token).get_api_key() if api_key: save_credentials(api_key) diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 77e11ef..c8c4c4d 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -497,12 +497,16 @@ def log_message(self, format, *args): def get_token() -> Optional[str]: """ Get the API token based on priority: - 1. Environment variable PENIFY_API_TOKEN + 1. Environment variable PENIFY_API_TOKEN from any .env file 2. Config file 'api_keys' value Returns: str or None: API token if found, None otherwise """ + # Ensure environment variables are loaded from all .env files + if DOTENV_AVAILABLE: + load_env_files() + # Check environment variable first env_token = get_env_var_or_default('PENIFY_API_TOKEN') if env_token: From 28fdd9d750eb503b5babe5f6a6ff3094b73bb03d Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 16:13:02 +0530 Subject: [PATCH 6/8] chore: remove deprecated .env file containing sensitive API information --- .env | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 41212f0..0000000 --- a/.env +++ /dev/null @@ -1,3 +0,0 @@ -PENIFY_LLM_MODEL=openai/deepseek-r1 -PENIFY_LLM_API_BASE=https://DeepSeek-R1-iegee.southcentralus.models.ai.azure.com/v1 -PENIFY_LLM_API_KEY=VZcWZ9PtJaKJdTGbXhksEiOdm0SEBtv7 From 30ebb606b9d4c7c0ecb45bd3fd5ef5dc8346cbf4 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 18:25:51 +0530 Subject: [PATCH 7/8] Refactor code structure for improved readability and maintainability --- .gitignore | 3 +- coverage.xml | 1468 -------------------------------------------------- 2 files changed, 2 insertions(+), 1469 deletions(-) delete mode 100644 coverage.xml diff --git a/.gitignore b/.gitignore index 6dffe7a..f0bd47e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ build/ *.log *.sqlite3 *.db -.env \ No newline at end of file +.env +coverage.xml \ No newline at end of file diff --git a/coverage.xml b/coverage.xml deleted file mode 100644 index e7e3625..0000000 --- a/coverage.xml +++ /dev/null @@ -1,1468 +0,0 @@ - - - - - - /Users/sumansaurabh/Documents/my-startup/penify-cli/penify_hook - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From f73df2d6ae4690eb627c89d7aaa063e918b31930 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Wed, 30 Apr 2025 18:31:27 +0530 Subject: [PATCH 8/8] chore: update .gitignore to include coverage and build artifacts --- .coverage | Bin 53248 -> 0 bytes .gitignore | 9 ++++++++- 2 files changed, 8 insertions(+), 1 deletion(-) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index 2b4ac98790af720935cd5e818d58e716ff71c916..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 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 W;7H{t&ryyeg`+G-8IJh<|NjNu^v10K diff --git a/.gitignore b/.gitignore index f0bd47e..aa93aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,11 @@ build/ *.sqlite3 *.db .env -coverage.xml \ No newline at end of file +coverage.xml +*.coverage +*.cover +*.egg +*.egg-info +*.whl +*.zip +.coverage \ No newline at end of file