From 915df7981af8e212fd8d86ba539604878f155c8a Mon Sep 17 00:00:00 2001 From: Rahul Subramaniam Date: Mon, 5 May 2025 08:03:52 +0530 Subject: [PATCH] upgrades --- .github/ISSUE_TEMPLATE/bug_report.md | 35 +++ .github/ISSUE_TEMPLATE/feature_request.md | 23 ++ .../pull_request_template.md | 22 ++ .github/workflows/python-tests.yml | 50 +++++ .gitignore | 57 +++++ CONTRIBUTING.md | 116 ++++++++++ LICENSE | 21 ++ README.md | 113 +++++++--- envprofile.py | 202 ----------------- pytest.ini | 6 + requirements.txt | 9 + run_tests.sh | 26 +++ setup.py | 57 +++++ src/envprofile/__init__.py | 7 + src/envprofile/__main__.py | 11 + src/envprofile/cli.py | 210 ++++++++++++++++++ src/envprofile/core.py | 164 ++++++++++++++ src/envprofile/main.py | 12 + tests/__init__.py | 1 + tests/test_cli.py | 203 +++++++++++++++++ tests/test_core.py | 144 ++++++++++++ tests/test_integration.py | 139 ++++++++++++ 22 files changed, 1401 insertions(+), 227 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/workflows/python-tests.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE delete mode 100644 envprofile.py create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100755 run_tests.sh create mode 100644 setup.py create mode 100644 src/envprofile/__init__.py create mode 100644 src/envprofile/__main__.py create mode 100644 src/envprofile/cli.py create mode 100644 src/envprofile/core.py create mode 100644 src/envprofile/main.py create mode 100644 tests/__init__.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_core.py create mode 100644 tests/test_integration.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..c9b8a33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: bug +assignees: '' +--- + +## Bug Description +A clear and concise description of what the bug is. + +## Steps To Reproduce +Steps to reproduce the behavior: +1. Run command '...' +2. Set up environment '...' +3. Execute '...' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +What actually happened instead. + +## Environment +- OS: [e.g. macOS 12.3, Ubuntu 22.04] +- Python Version: [e.g. 3.9.10] +- EnvProfile Version: [e.g. 0.1.0] +- Shell: [e.g. bash, zsh, fish] + +## Additional Context +Add any other context about the problem here, such as configuration files or error logs. + +## Possible Solution +If you have ideas on how to fix the issue, please share them here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..96b08d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE] " +labels: enhancement +assignees: '' +--- + +## Feature Description +A clear and concise description of the feature you'd like to see implemented. + +## Use Case +Describe the specific use case or problem this feature would solve. +Example: "When working with multiple environment sets, I need to..." + +## Proposed Solution +Describe your suggested solution or implementation approach. + +## Alternatives Considered +Describe any alternative solutions or workarounds you've considered. + +## Additional Context +Add any other context, screenshots, or examples about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..1925d95 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,22 @@ +## Description +Please include a summary of the change and which issue is fixed. Include relevant motivation and context. + +Fixes # (issue) + +## Type of change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update + +## How Has This Been Tested? +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. + +## Checklist: +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes \ No newline at end of file diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..bd541a6 --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,50 @@ +name: Python Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -e . + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings + flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics + + - name: Type check with mypy + run: | + mypy src + + - name: Test with pytest + run: | + pytest --cov=envprofile + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + fail_ci_if_error: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ccac6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Project specific +.config/ +envprofile.py.old \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2fa64d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing to EnvProfile + +Thank you for your interest in contributing to EnvProfile! This document provides guidelines and instructions for contributing. + +## Code of Conduct + +Please be respectful and considerate of others when participating in this project. + +## Getting Started + +### Setup Development Environment + +1. Clone the repository: + ```bash + git clone https://github.com/yourusername/envprofile.git + cd envprofile + ``` + +2. Create a virtual environment and install development dependencies: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + pip install -e ".[dev]" + pip install -r requirements.txt + ``` + +3. Run the tests to verify your setup: + ```bash + pytest + ``` + +## Development Workflow + +### Branching Strategy + +- Use `main` branch for stable releases +- Create feature branches from `main` named with the pattern: `feature/your-feature-name` +- Create bug fix branches with the pattern: `bugfix/issue-description` + +### Making Changes + +1. Create a new branch for your changes +2. Make your changes with clear, descriptive commit messages +3. Add tests for your changes +4. Run tests and ensure all pass +5. Update documentation if necessary +6. Submit a pull request + +### Code Style + +We follow PEP 8 guidelines for Python code. Use the following tools to ensure your code conforms to our style: + +- **Black**: For code formatting + ```bash + black src tests + ``` + +- **Flake8**: For style guide enforcement + ```bash + flake8 src tests + ``` + +- **MyPy**: For type checking + ```bash + mypy src + ``` + +### Testing + +All changes should include tests. Run the test suite with: + +```bash +pytest +``` + +To see test coverage information: + +```bash +pytest --cov=envprofile +``` + +## Pull Request Process + +1. Ensure all tests pass and code style checks pass +2. Update the README.md or documentation with details of changes if applicable +3. The PR should work for Python 3.6 and later versions +4. Include a clear and descriptive PR title and description +5. Link any related issues in the PR description + +## Release Process + +Releases are handled by the project maintainers. The process typically involves: + +1. Updating the version in `src/envprofile/__init__.py` +2. Updating the changelog +3. Creating a tagged release + +## Documentation + +- Update the README.md file for user-focused documentation +- Add docstrings for all public modules, functions, classes, and methods +- Use clear and descriptive variable names and comments + +## Bug Reports and Feature Requests + +Please use the GitHub issue tracker to report bugs or request features. When reporting bugs: + +1. Check if the bug has already been reported +2. Use a clear and descriptive title +3. Provide detailed steps to reproduce the bug +4. Include expected and actual behavior +5. Provide your environment details (OS, Python version, etc.) + +## Contact + +If you have questions about contributing, please open an issue on GitHub or contact the maintainers directly. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7f1ce40 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 EnvProfile Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 7e37b71..593eae1 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,54 @@ # EnvProfile - Environment Variable Profile Manager -This tool lets you create and manage different sets of environment variables that you can easily load when needed. +[![Python Tests](https://github.com/yourusername/envprofile/actions/workflows/python-tests.yml/badge.svg)](https://github.com/yourusername/envprofile/actions/workflows/python-tests.yml) +[![PyPI version](https://badge.fury.io/py/envprofile.svg)](https://badge.fury.io/py/envprofile) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +EnvProfile is a tool that lets you create and manage different sets of environment variables that you can easily load when needed. This is particularly useful for developers who work with different projects that require different environment configurations. + +## Features + +- Create multiple named environment profiles +- Add, remove, and update environment variables in profiles +- Load environment variables from profiles into your current shell +- List available profiles and view their contents +- Easy to use command-line interface ## Installation -1. Save the script to a location in your PATH (e.g., `/usr/local/bin/envprofile` or `~/bin/envprofile`): +### From PyPI (Recommended) + +```bash +pip install envprofile +``` + +### From Source + +```bash +# Clone the repository +git clone https://github.com/yourusername/envprofile.git +cd envprofile + +# Install the package +pip install -e . +``` - ```bash - # Download the script - curl -o /usr/local/bin/envprofile https://your-host/path/to/envprofile.py - - # Or manually create the file and paste the code in it - - # Make it executable - chmod +x /usr/local/bin/envprofile - - # Make sure /usr/local/bin is in your PATH (add to your .bashrc or .zshrc if needed) - export PATH="/usr/local/bin:$PATH" - ``` +### Shell Integration -2. Add a shell function to your `.bashrc`, `.zshrc`, or equivalent shell configuration file: +Add a shell function to your `.bashrc`, `.zshrc`, or equivalent shell configuration file: - ```bash - # Add this to your shell configuration file - function use-env() { - eval "$(envprofile load $1)" - } - ``` +```bash +# Add this to your shell configuration file +function use-env() { + eval "$(envprofile load $1)" +} +``` -3. Reload your shell configuration: +Reload your shell configuration: - ```bash - source ~/.bashrc # or ~/.zshrc - ``` +```bash +source ~/.bashrc # or ~/.zshrc +``` ## Usage Examples @@ -108,3 +124,50 @@ All profiles are stored in a single JSON file at `~/.config/envprofile/profiles. } } ``` + +## Advanced Usage + +### Using with Docker + +You can use EnvProfile to generate environment files for Docker: + +```bash +# Generate a .env file for docker-compose +envprofile load dev > .env +``` + +### Using with Multiple Projects + +Create project-specific profiles by using prefixes: + +```bash +# Create profiles for different projects +envprofile create project1-dev +envprofile create project1-prod +envprofile create project2-dev +``` + +## Troubleshooting + +### Common Issues + +**Issue**: Changes to environment variables not appearing in the shell + +**Solution**: Make sure you're using the `eval` command or the `use-env` function as described in the installation section. + +**Issue**: Error "command not found: envprofile" + +**Solution**: Ensure that the installation directory is in your PATH or install using pip. + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- Inspired by various environment management tools like direnv and autoenv +- Thanks to all contributors diff --git a/envprofile.py b/envprofile.py deleted file mode 100644 index 2a5380e..0000000 --- a/envprofile.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 -""" -envprofile - A tool to manage and load environment variable profiles - -Usage: - envprofile create - Create a new profile - envprofile add - Add/update an environment variable to a profile - envprofile remove - Remove an environment variable from a profile - envprofile list - List all available profiles - envprofile show - Show details of a specific profile - envprofile load - Load environment variables from a profile - envprofile delete - Delete a profile - envprofile help - Show this help message - -When using 'load', you need to source the output for it to affect your current shell: - eval $(envprofile load dev) -""" - -import os -import sys -import json -import argparse -from pathlib import Path - -CONFIG_DIR = Path.home() / '.config' / 'envprofile' -CONFIG_FILE = CONFIG_DIR / 'profiles.json' - -def ensure_config_exists(): - """Ensure the config directory and file exist""" - if not CONFIG_DIR.exists(): - CONFIG_DIR.mkdir(parents=True) - - if not CONFIG_FILE.exists(): - with open(CONFIG_FILE, 'w') as f: - json.dump({}, f, indent=2) - -def load_profiles(): - """Load profiles from the config file""" - ensure_config_exists() - with open(CONFIG_FILE, 'r') as f: - try: - return json.load(f) - except json.JSONDecodeError: - return {} - -def save_profiles(profiles): - """Save profiles to the config file""" - ensure_config_exists() - with open(CONFIG_FILE, 'w') as f: - json.dump(profiles, f, indent=2) - -def create_profile(args): - """Create a new profile""" - profiles = load_profiles() - if args.profile_name in profiles: - print(f"Profile '{args.profile_name}' already exists") - return - - profiles[args.profile_name] = {} - save_profiles(profiles) - print(f"Created profile '{args.profile_name}'") - -def add_variable(args): - """Add or update an environment variable to a profile""" - profiles = load_profiles() - if args.profile_name not in profiles: - print(f"Profile '{args.profile_name}' does not exist") - return - - profiles[args.profile_name][args.key] = args.value - save_profiles(profiles) - print(f"Added/updated '{args.key}={args.value}' to profile '{args.profile_name}'") - -def remove_variable(args): - """Remove an environment variable from a profile""" - profiles = load_profiles() - if args.profile_name not in profiles: - print(f"Profile '{args.profile_name}' does not exist") - return - - if args.key not in profiles[args.profile_name]: - print(f"Key '{args.key}' does not exist in profile '{args.profile_name}'") - return - - del profiles[args.profile_name][args.key] - save_profiles(profiles) - print(f"Removed '{args.key}' from profile '{args.profile_name}'") - -def list_profiles(args): - """List all available profiles""" - profiles = load_profiles() - if not profiles: - print("No profiles available") - return - - print("Available profiles:") - for profile in profiles: - var_count = len(profiles[profile]) - print(f" - {profile} ({var_count} variable{'s' if var_count != 1 else ''})") - -def show_profile(args): - """Show details of a specific profile""" - profiles = load_profiles() - if args.profile_name not in profiles: - print(f"Profile '{args.profile_name}' does not exist") - return - - profile = profiles[args.profile_name] - if not profile: - print(f"Profile '{args.profile_name}' is empty") - return - - print(f"Profile: {args.profile_name}") - print("Environment variables:") - for key, value in profile.items(): - print(f" {key}={value}") - -def load_profile(args): - """Load environment variables from a profile - - This outputs shell commands that need to be evaluated in the current shell. - Usage: eval $(envprofile load profile_name) - """ - profiles = load_profiles() - if args.profile_name not in profiles: - print(f"# Profile '{args.profile_name}' does not exist", file=sys.stderr) - return - - profile = profiles[args.profile_name] - if not profile: - print(f"# Profile '{args.profile_name}' is empty", file=sys.stderr) - return - - # Generate shell export commands - for key, value in profile.items(): - # Properly escape the value for shell - escaped_value = value.replace("'", "'\\''") - print(f"export {key}='{escaped_value}';") - -def delete_profile(args): - """Delete a profile""" - profiles = load_profiles() - if args.profile_name not in profiles: - print(f"Profile '{args.profile_name}' does not exist") - return - - del profiles[args.profile_name] - save_profiles(profiles) - print(f"Deleted profile '{args.profile_name}'") - -def main(): - parser = argparse.ArgumentParser(description='Manage environment variable profiles') - subparsers = parser.add_subparsers(dest='command', help='Command') - - # Create profile - create_parser = subparsers.add_parser('create', help='Create a new profile') - create_parser.add_argument('profile_name', help='Name of the profile') - create_parser.set_defaults(func=create_profile) - - # Add variable - add_parser = subparsers.add_parser('add', help='Add/update an environment variable to a profile') - add_parser.add_argument('profile_name', help='Name of the profile') - add_parser.add_argument('key', help='Environment variable name') - add_parser.add_argument('value', help='Environment variable value') - add_parser.set_defaults(func=add_variable) - - # Remove variable - remove_parser = subparsers.add_parser('remove', help='Remove an environment variable from a profile') - remove_parser.add_argument('profile_name', help='Name of the profile') - remove_parser.add_argument('key', help='Environment variable name') - remove_parser.set_defaults(func=remove_variable) - - # List profiles - list_parser = subparsers.add_parser('list', help='List all available profiles') - list_parser.set_defaults(func=list_profiles) - - # Show profile - show_parser = subparsers.add_parser('show', help='Show details of a specific profile') - show_parser.add_argument('profile_name', help='Name of the profile') - show_parser.set_defaults(func=show_profile) - - # Load profile - load_parser = subparsers.add_parser('load', help='Load environment variables from a profile') - load_parser.add_argument('profile_name', help='Name of the profile') - load_parser.set_defaults(func=load_profile) - - # Delete profile - delete_parser = subparsers.add_parser('delete', help='Delete a profile') - delete_parser.add_argument('profile_name', help='Name of the profile') - delete_parser.set_defaults(func=delete_profile) - - args = parser.parse_args() - - if not args.command or args.command == 'help': - parser.print_help() - print(__doc__) - return - - args.func(args) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..cd0f301 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a18a6a1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# Production dependencies +# (none for this project as it uses standard library only) + +# Development dependencies +pytest>=7.0.0 +pytest-cov>=3.0.0 +black>=22.1.0 +flake8>=5.0.0 +mypy>=0.950 \ No newline at end of file diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..6895e9d --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Script to run tests and code quality checks + +set -e # Exit on any error + +# Use the Python where we installed our package +PYTHON="python3.9" # This should match the version we installed the package with + +# Run code formatting +echo "Running Black code formatter..." +$PYTHON -m black src tests + +# Run linting +echo "Running Flake8 linter..." +$PYTHON -m flake8 src tests --max-line-length=100 + +# Run type checking +echo "Running MyPy type checker..." +$PYTHON -m mypy src + +# Run tests with coverage +echo "Running tests without coverage..." +$PYTHON -m pytest + +# Show success message +echo "All tests and checks passed!" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ce9f4f3 --- /dev/null +++ b/setup.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Setup script for EnvProfile package.""" + +from setuptools import setup, find_packages +import os + +# Read version from package __init__.py +with open(os.path.join("src", "envprofile", "__init__.py"), "r") as f: + for line in f: + if line.startswith("__version__"): + version = line.split("=")[1].strip().strip('"').strip("'") + break + +# Read long description from README.md +with open("README.md", "r") as f: + long_description = f.read() + +setup( + name="envprofile", + version=version, + description="Environment Variable Profile Manager", + long_description=long_description, + long_description_content_type="text/markdown", + author="EnvProfile Authors", + author_email="your@email.com", + url="https://github.com/yourusername/envprofile", + packages=find_packages(where="src"), + package_dir={"": "src"}, + python_requires=">=3.6", + entry_points={ + "console_scripts": [ + "envprofile=envprofile.cli:main", + ], + }, + extras_require={ + "dev": [ + "pytest>=7.0.0", + "pytest-cov>=3.0.0", + "black>=22.1.0", + "flake8>=5.0.0", + "mypy>=0.950", + ], + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", + ], +) \ No newline at end of file diff --git a/src/envprofile/__init__.py b/src/envprofile/__init__.py new file mode 100644 index 0000000..c29140e --- /dev/null +++ b/src/envprofile/__init__.py @@ -0,0 +1,7 @@ +""" +EnvProfile - Environment Variable Profile Manager + +A tool to manage and load environment variable profiles. +""" + +__version__ = "0.1.0" diff --git a/src/envprofile/__main__.py b/src/envprofile/__main__.py new file mode 100644 index 0000000..e464c85 --- /dev/null +++ b/src/envprofile/__main__.py @@ -0,0 +1,11 @@ +""" +Main entry point for the EnvProfile package. + +This allows the package to be run as a module (python -m envprofile). +""" + +import sys +from .cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/envprofile/cli.py b/src/envprofile/cli.py new file mode 100644 index 0000000..a4ffedc --- /dev/null +++ b/src/envprofile/cli.py @@ -0,0 +1,210 @@ +""" +Command-line interface for EnvProfile. + +This module provides the command-line interface for interacting with the EnvProfile tool. +""" + +import sys +import argparse +from typing import List, Optional + +from .core import ProfileManager + + +def create_profile_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """Create a new profile command handler.""" + if manager.create_profile(args.profile_name): + print(f"Created profile '{args.profile_name}'") + return 0 + else: + print(f"Profile '{args.profile_name}' already exists") + return 1 + + +def add_variable_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """Add a variable to a profile command handler.""" + if manager.add_variable(args.profile_name, args.key, args.value): + msg = ( + f"Added/updated '{args.key}={args.value}' to profile '{args.profile_name}'" + ) + print(msg) + return 0 + else: + print(f"Profile '{args.profile_name}' does not exist") + return 1 + + +def remove_variable_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """Remove a variable from a profile command handler.""" + result = manager.remove_variable(args.profile_name, args.key) + if not result: + profiles = manager.load_profiles() + if args.profile_name not in profiles: + print(f"Profile '{args.profile_name}' does not exist") + else: + key = args.key + profile = args.profile_name + print(f"Key '{key}' does not exist in profile '{profile}'") + return 1 + + print(f"Removed '{args.key}' from profile '{args.profile_name}'") + return 0 + + +def list_profiles_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """List all profiles command handler.""" + profiles = manager.list_profiles() + if not profiles: + print("No profiles available") + return 0 + + print("Available profiles:") + for profile, var_count in profiles.items(): + suffix = "s" if var_count != 1 else "" + print(f" - {profile} ({var_count} variable{suffix})") + + return 0 + + +def show_profile_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """Show a profile's details command handler.""" + profile = manager.get_profile(args.profile_name) + if profile is None: + print(f"Profile '{args.profile_name}' does not exist") + return 1 + + if not profile: + print(f"Profile '{args.profile_name}' is empty") + return 0 + + print(f"Profile: {args.profile_name}") + print("Environment variables:") + for key, value in profile.items(): + print(f" {key}={value}") + + return 0 + + +def load_profile_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """Load a profile's environment variables command handler.""" + profile = manager.get_profile(args.profile_name) + if profile is None: + msg = f"# Profile '{args.profile_name}' does not exist" + print(msg, file=sys.stderr) + return 1 + + if not profile: + print(f"# Profile '{args.profile_name}' is empty", file=sys.stderr) + return 0 + + # Generate shell export commands + for key, value in profile.items(): + # Properly escape the value for shell + escaped_value = value.replace("'", "'\\''") + print(f"export {key}='{escaped_value}';") + + return 0 + + +def delete_profile_cmd(manager: ProfileManager, args: argparse.Namespace) -> int: + """Delete a profile command handler.""" + if manager.delete_profile(args.profile_name): + print(f"Deleted profile '{args.profile_name}'") + return 0 + else: + print(f"Profile '{args.profile_name}' does not exist") + return 1 + + +def get_parser() -> argparse.ArgumentParser: + """Create and return the argument parser.""" + parser = argparse.ArgumentParser( + description="Manage environment variable profiles", + epilog=""" +When using 'load', you need to source the output for it to affect your current shell: + eval $(envprofile load dev) + +Or use the provided shell function: + use-env dev + """, + ) + + subparsers = parser.add_subparsers(dest="command", help="Command") + + # Create profile + create_parser = subparsers.add_parser("create", help="Create a new profile") + create_parser.add_argument("profile_name", help="Name of the profile") + + # Add variable + add_parser = subparsers.add_parser( + "add", help="Add/update an environment variable to a profile" + ) + add_parser.add_argument("profile_name", help="Name of the profile") + add_parser.add_argument("key", help="Environment variable name") + add_parser.add_argument("value", help="Environment variable value") + + # Remove variable + remove_parser = subparsers.add_parser( + "remove", help="Remove an environment variable from a profile" + ) + remove_parser.add_argument("profile_name", help="Name of the profile") + remove_parser.add_argument("key", help="Environment variable name") + + # List profiles + subparsers.add_parser("list", help="List all available profiles") + + # Show profile + show_parser = subparsers.add_parser( + "show", help="Show details of a specific profile" + ) + show_parser.add_argument("profile_name", help="Name of the profile") + + # Load profile + load_parser = subparsers.add_parser( + "load", help="Load environment variables from a profile" + ) + load_parser.add_argument("profile_name", help="Name of the profile") + + # Delete profile + delete_parser = subparsers.add_parser("delete", help="Delete a profile") + delete_parser.add_argument("profile_name", help="Name of the profile") + + return parser + + +def main(args: Optional[List[str]] = None) -> int: + """ + Main entry point for the CLI. + + Args: + args: Command line arguments (if None, sys.argv[1:] is used) + + Returns: + Exit code (0 for success, non-zero for errors) + """ + parser = get_parser() + parsed_args = parser.parse_args(args) + + if not parsed_args.command: + parser.print_help() + return 0 + + manager = ProfileManager() + + # Command -> function mapping + commands = { + "create": create_profile_cmd, + "add": add_variable_cmd, + "remove": remove_variable_cmd, + "list": list_profiles_cmd, + "show": show_profile_cmd, + "load": load_profile_cmd, + "delete": delete_profile_cmd, + } + + # Execute the corresponding function + return commands[parsed_args.command](manager, parsed_args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/envprofile/core.py b/src/envprofile/core.py new file mode 100644 index 0000000..ac28ca1 --- /dev/null +++ b/src/envprofile/core.py @@ -0,0 +1,164 @@ +""" +Core functionality for the EnvProfile package. + +This module handles the loading, saving, and manipulation of environment profiles. +""" + +import json +from pathlib import Path +from typing import Dict, Optional + + +class ProfileManager: + """Manages environment variable profiles.""" + + def __init__(self, config_dir: Optional[Path] = None): + """ + Initialize the profile manager. + + Args: + config_dir: Optional directory path for configuration files. + If None, defaults to ~/.config/envprofile + """ + if config_dir is None: + self.config_dir = Path.home() / ".config" / "envprofile" + else: + self.config_dir = config_dir + + self.config_file = self.config_dir / "profiles.json" + self.ensure_config_exists() + + def ensure_config_exists(self) -> None: + """Ensure the config directory and file exist.""" + if not self.config_dir.exists(): + self.config_dir.mkdir(parents=True) + + if not self.config_file.exists(): + with open(self.config_file, "w") as f: + json.dump({}, f, indent=2) + + def load_profiles(self) -> Dict[str, Dict[str, str]]: + """ + Load profiles from the config file. + + Returns: + Dict mapping profile names to their environment variables + """ + self.ensure_config_exists() + with open(self.config_file, "r") as f: + try: + return json.load(f) + except json.JSONDecodeError: + return {} + + def save_profiles(self, profiles: Dict[str, Dict[str, str]]) -> None: + """ + Save profiles to the config file. + + Args: + profiles: Dict mapping profile names to their environment variables + """ + self.ensure_config_exists() + with open(self.config_file, "w") as f: + json.dump(profiles, f, indent=2) + + def create_profile(self, profile_name: str) -> bool: + """ + Create a new profile. + + Args: + profile_name: Name of the profile to create + + Returns: + True if profile was created, False if it already exists + """ + profiles = self.load_profiles() + if profile_name in profiles: + return False + + profiles[profile_name] = {} + self.save_profiles(profiles) + return True + + def add_variable(self, profile_name: str, key: str, value: str) -> bool: + """ + Add or update an environment variable to a profile. + + Args: + profile_name: Name of the profile + key: Environment variable name + value: Environment variable value + + Returns: + True if successful, False if profile doesn't exist + """ + profiles = self.load_profiles() + if profile_name not in profiles: + return False + + profiles[profile_name][key] = value + self.save_profiles(profiles) + return True + + def remove_variable(self, profile_name: str, key: str) -> bool: + """ + Remove an environment variable from a profile. + + Args: + profile_name: Name of the profile + key: Environment variable name to remove + + Returns: + True if successful, False if profile or key doesn't exist + """ + profiles = self.load_profiles() + if profile_name not in profiles: + return False + + if key not in profiles[profile_name]: + return False + + del profiles[profile_name][key] + self.save_profiles(profiles) + return True + + def get_profile(self, profile_name: str) -> Optional[Dict[str, str]]: + """ + Get a specific profile's environment variables. + + Args: + profile_name: Name of the profile + + Returns: + Dict of environment variables or None if profile doesn't exist + """ + profiles = self.load_profiles() + return profiles.get(profile_name) + + def list_profiles(self) -> Dict[str, int]: + """ + Get all available profiles with their variable counts. + + Returns: + Dict mapping profile names to their variable counts + """ + profiles = self.load_profiles() + return {name: len(variables) for name, variables in profiles.items()} + + def delete_profile(self, profile_name: str) -> bool: + """ + Delete a profile. + + Args: + profile_name: Name of the profile to delete + + Returns: + True if successful, False if profile doesn't exist + """ + profiles = self.load_profiles() + if profile_name not in profiles: + return False + + del profiles[profile_name] + self.save_profiles(profiles) + return True diff --git a/src/envprofile/main.py b/src/envprofile/main.py new file mode 100644 index 0000000..6682cda --- /dev/null +++ b/src/envprofile/main.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +""" +Main executable script for EnvProfile. + +This is the main entry point for direct execution as a script. +""" + +import sys +from .cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..396ecc3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for EnvProfile.""" diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..d1d0b2c --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,203 @@ +"""Tests for the CLI functionality of EnvProfile.""" + +import io +import sys +import tempfile +import unittest +from unittest.mock import patch, MagicMock +from pathlib import Path + +from envprofile.cli import main + + +class TestCLI(unittest.TestCase): + """Test suite for the CLI interface.""" + + def setUp(self): + """Set up test fixtures.""" + # Create a temporary directory for test files + self.temp_dir = tempfile.TemporaryDirectory() + self.config_dir = Path(self.temp_dir.name) + + # Create a patcher for ProfileManager + self.patcher = patch("envprofile.cli.ProfileManager") + self.mock_profile_manager_class = self.patcher.start() + + # Create a mock instance that will be returned by ProfileManager() + self.mock_manager = MagicMock() + self.mock_profile_manager_class.return_value = self.mock_manager + + def tearDown(self): + """Tear down test fixtures.""" + self.patcher.stop() + self.temp_dir.cleanup() + + def test_create_profile(self): + """Test the create profile command.""" + # Set up the mock to return True (success) + self.mock_manager.create_profile.return_value = True + + # Capture stdout + captured_output = io.StringIO() + sys.stdout = captured_output + + # Call the function + exit_code = main(["create", "test"]) + + # Reset stdout + sys.stdout = sys.__stdout__ + + # Check that the correct method was called + self.mock_manager.create_profile.assert_called_once_with("test") + + # Check the output + self.assertIn("Created profile 'test'", captured_output.getvalue()) + + # Check the exit code + self.assertEqual(exit_code, 0) + + # Now test the failure case + self.mock_manager.create_profile.return_value = False + + captured_output = io.StringIO() + sys.stdout = captured_output + + exit_code = main(["create", "test"]) + + sys.stdout = sys.__stdout__ + + self.assertIn("already exists", captured_output.getvalue()) + self.assertEqual(exit_code, 1) + + def test_add_variable(self): + """Test the add variable command.""" + # Set up the mock to return True (success) + self.mock_manager.add_variable.return_value = True + + # Capture stdout + captured_output = io.StringIO() + sys.stdout = captured_output + + # Call the function + exit_code = main(["add", "test", "KEY", "value"]) + + # Reset stdout + sys.stdout = sys.__stdout__ + + # Check that the correct method was called + self.mock_manager.add_variable.assert_called_once_with("test", "KEY", "value") + + # Check the output + self.assertIn( + "Added/updated 'KEY=value' to profile 'test'", captured_output.getvalue() + ) + + # Check the exit code + self.assertEqual(exit_code, 0) + + # Now test the failure case + self.mock_manager.add_variable.return_value = False + + captured_output = io.StringIO() + sys.stdout = captured_output + + exit_code = main(["add", "test", "KEY", "value"]) + + sys.stdout = sys.__stdout__ + + self.assertIn("does not exist", captured_output.getvalue()) + self.assertEqual(exit_code, 1) + + def test_list_profiles(self): + """Test the list profiles command.""" + # Set up the mock to return a dict of profiles + self.mock_manager.list_profiles.return_value = {"test1": 1, "test2": 2} + + # Capture stdout + captured_output = io.StringIO() + sys.stdout = captured_output + + # Call the function + exit_code = main(["list"]) + + # Reset stdout + sys.stdout = sys.__stdout__ + + # Check that the correct method was called + self.mock_manager.list_profiles.assert_called_once() + + # Check the output + output = captured_output.getvalue() + self.assertIn("Available profiles:", output) + self.assertIn("test1 (1 variable)", output) + self.assertIn("test2 (2 variables)", output) + + # Check the exit code + self.assertEqual(exit_code, 0) + + # Now test the empty case + self.mock_manager.list_profiles.return_value = {} + + captured_output = io.StringIO() + sys.stdout = captured_output + + exit_code = main(["list"]) + + sys.stdout = sys.__stdout__ + + self.assertIn("No profiles available", captured_output.getvalue()) + self.assertEqual(exit_code, 0) + + def test_load_profile(self): + """Test the load profile command.""" + # Set up the mock to return a profile + self.mock_manager.get_profile.return_value = { + "KEY1": "value1", + "KEY2": "value with spaces", + } + + # Capture stdout + captured_output = io.StringIO() + sys.stdout = captured_output + + # Call the function + exit_code = main(["load", "test"]) + + # Reset stdout + sys.stdout = sys.__stdout__ + + # Check that the correct method was called + self.mock_manager.get_profile.assert_called_once_with("test") + + # Check the output + output = captured_output.getvalue() + # Split assertions to avoid long lines + self.assertIn("export KEY1='value1';", output) + self.assertIn("export KEY2='value with spaces';", output) + + # Check the exit code + self.assertEqual(exit_code, 0) + + # Now test when profile doesn't exist + self.mock_manager.get_profile.return_value = None + + # Capture stderr + captured_error = io.StringIO() + sys.stderr = captured_error + + # Capture stdout + captured_output = io.StringIO() + sys.stdout = captured_output + + exit_code = main(["load", "test"]) + + # Reset stderr and stdout + sys.stderr = sys.__stderr__ + sys.stdout = sys.__stdout__ + + self.assertIn("does not exist", captured_error.getvalue()) + self.assertEqual(exit_code, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..21f31e2 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,144 @@ +"""Tests for the core functionality of EnvProfile.""" + +import json +import tempfile +from pathlib import Path +import unittest + +from envprofile.core import ProfileManager + + +class TestProfileManager(unittest.TestCase): + """Test suite for the ProfileManager class.""" + + def setUp(self): + """Set up test fixtures.""" + # Create a temporary directory for test files + self.temp_dir = tempfile.TemporaryDirectory() + self.config_dir = Path(self.temp_dir.name) + self.manager = ProfileManager(self.config_dir) + + def tearDown(self): + """Tear down test fixtures.""" + self.temp_dir.cleanup() + + def test_ensure_config_exists(self): + """Test that ensure_config_exists creates the config directory and file.""" + # Config directory and file should have been created in setUp + self.assertTrue(self.config_dir.exists()) + self.assertTrue(self.manager.config_file.exists()) + + # File should contain an empty JSON object + with open(self.manager.config_file, "r") as f: + content = json.load(f) + self.assertEqual(content, {}) + + def test_create_profile(self): + """Test creating a new profile.""" + # Create a new profile + result = self.manager.create_profile("test") + self.assertTrue(result) + + # Profile should exist in the profiles file + with open(self.manager.config_file, "r") as f: + content = json.load(f) + self.assertIn("test", content) + self.assertEqual(content["test"], {}) + + # Creating the same profile again should return False + result = self.manager.create_profile("test") + self.assertFalse(result) + + def test_add_variable(self): + """Test adding a variable to a profile.""" + # Create a profile + self.manager.create_profile("test") + + # Add a variable + result = self.manager.add_variable("test", "KEY", "value") + self.assertTrue(result) + + # Variable should exist in the profile + with open(self.manager.config_file, "r") as f: + content = json.load(f) + self.assertEqual(content["test"]["KEY"], "value") + + # Adding to non-existent profile should return False + result = self.manager.add_variable("nonexistent", "KEY", "value") + self.assertFalse(result) + + def test_remove_variable(self): + """Test removing a variable from a profile.""" + # Create a profile and add a variable + self.manager.create_profile("test") + self.manager.add_variable("test", "KEY", "value") + + # Remove the variable + result = self.manager.remove_variable("test", "KEY") + self.assertTrue(result) + + # Variable should no longer exist in the profile + with open(self.manager.config_file, "r") as f: + content = json.load(f) + self.assertNotIn("KEY", content["test"]) + + # Removing from non-existent profile should return False + result = self.manager.remove_variable("nonexistent", "KEY") + self.assertFalse(result) + + # Removing non-existent key should return False + result = self.manager.remove_variable("test", "NONEXISTENT") + self.assertFalse(result) + + def test_get_profile(self): + """Test getting a profile.""" + # Create a profile and add variables + self.manager.create_profile("test") + self.manager.add_variable("test", "KEY1", "value1") + self.manager.add_variable("test", "KEY2", "value2") + + # Get the profile + profile = self.manager.get_profile("test") + self.assertEqual(profile, {"KEY1": "value1", "KEY2": "value2"}) + + # Getting non-existent profile should return None + profile = self.manager.get_profile("nonexistent") + self.assertIsNone(profile) + + def test_list_profiles(self): + """Test listing profiles.""" + # Create several profiles with variables + self.manager.create_profile("test1") + self.manager.add_variable("test1", "KEY1", "value1") + + self.manager.create_profile("test2") + self.manager.add_variable("test2", "KEY1", "value1") + self.manager.add_variable("test2", "KEY2", "value2") + + self.manager.create_profile("empty") + + # List profiles + profiles = self.manager.list_profiles() + self.assertEqual(profiles, {"test1": 1, "test2": 2, "empty": 0}) + + def test_delete_profile(self): + """Test deleting a profile.""" + # Create a profile + self.manager.create_profile("test") + + # Delete the profile + result = self.manager.delete_profile("test") + self.assertTrue(result) + + # Profile should no longer exist + with open(self.manager.config_file, "r") as f: + content = json.load(f) + self.assertNotIn("test", content) + + # Deleting non-existent profile should return False + result = self.manager.delete_profile("nonexistent") + self.assertFalse(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..fa859a3 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,139 @@ +"""Integration tests for the EnvProfile package.""" + +import os +import json +import tempfile +import unittest +import subprocess +from pathlib import Path +import sys + + +class TestIntegration(unittest.TestCase): + """Integration test suite for EnvProfile.""" + + def setUp(self): + """Set up test fixtures.""" + # Create a temporary directory for test files + self.temp_dir = tempfile.TemporaryDirectory() + self.config_dir = Path(self.temp_dir.name) / ".config" / "envprofile" + self.config_dir.mkdir(parents=True) + self.config_file = self.config_dir / "profiles.json" + + # Create an empty profiles file + with open(self.config_file, "w") as f: + json.dump({}, f) + + # Set environment variable to point to our test config + self.original_home = os.environ.get("HOME") + os.environ["HOME"] = str(self.temp_dir.name) + + # Path to the module + self.module_path = "envprofile.cli" + + def tearDown(self): + """Tear down test fixtures.""" + # Restore original HOME environment variable + if self.original_home: + os.environ["HOME"] = self.original_home + else: + del os.environ["HOME"] + + self.temp_dir.cleanup() + + def run_command(self, args): + """Run a command using the CLI module and return the output and exit code.""" + cmd = [sys.executable, "-m", self.module_path] + args + process = subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False + ) + return process.stdout, process.stderr, process.returncode + + def test_full_workflow(self): + """Test a full workflow of creating, adding, listing, and loading profiles.""" + # Create a profile + stdout, stderr, exit_code = self.run_command(["create", "dev"]) + self.assertEqual(exit_code, 0) + self.assertIn("Created profile 'dev'", stdout) + + # Add some variables + cmd = ["add", "dev", "DB_HOST", "localhost"] + stdout, stderr, exit_code = self.run_command(cmd) + self.assertEqual(exit_code, 0) + + expected = "Added/updated 'DB_HOST=localhost' to profile 'dev'" + self.assertIn(expected, stdout) + + stdout, stderr, exit_code = self.run_command(["add", "dev", "DB_PORT", "5432"]) + self.assertEqual(exit_code, 0) + + # List profiles + stdout, stderr, exit_code = self.run_command(["list"]) + self.assertEqual(exit_code, 0) + self.assertIn("dev (2 variables)", stdout) + + # Show profile + stdout, stderr, exit_code = self.run_command(["show", "dev"]) + self.assertEqual(exit_code, 0) + self.assertIn("DB_HOST=localhost", stdout) + self.assertIn("DB_PORT=5432", stdout) + + # Load profile + stdout, stderr, exit_code = self.run_command(["load", "dev"]) + self.assertEqual(exit_code, 0) + + # Check output for environment variables + self.assertIn("export DB_HOST='localhost';", stdout) + self.assertIn("export DB_PORT='5432';", stdout) + + # Remove a variable + cmd = ["remove", "dev", "DB_PORT"] + stdout, stderr, exit_code = self.run_command(cmd) + self.assertEqual(exit_code, 0) + self.assertIn("Removed 'DB_PORT' from profile 'dev'", stdout) + + # Show profile again to verify removal + stdout, stderr, exit_code = self.run_command(["show", "dev"]) + self.assertEqual(exit_code, 0) + self.assertIn("DB_HOST=localhost", stdout) + self.assertNotIn("DB_PORT=5432", stdout) + + # Delete profile + stdout, stderr, exit_code = self.run_command(["delete", "dev"]) + self.assertEqual(exit_code, 0) + self.assertIn("Deleted profile 'dev'", stdout) + + # List profiles to verify deletion + stdout, stderr, exit_code = self.run_command(["list"]) + self.assertEqual(exit_code, 0) + self.assertIn("No profiles available", stdout) + + def test_error_handling(self): + """Test error handling for various scenarios.""" + # Try to show a non-existent profile + profile_cmd = ["show", "nonexistent"] + stdout, stderr, exit_code = self.run_command(profile_cmd) + self.assertEqual(exit_code, 1) + self.assertIn("does not exist", stdout) + + # Try to add a variable to a non-existent profile + cmd = ["add", "nonexistent", "KEY", "value"] + stdout, stderr, exit_code = self.run_command(cmd) + self.assertEqual(exit_code, 1) + self.assertIn("does not exist", stdout) + + # Try to remove a variable from a non-existent profile + cmd = ["remove", "nonexistent", "KEY"] + stdout, stderr, exit_code = self.run_command(cmd) + self.assertEqual(exit_code, 1) + self.assertIn("does not exist", stdout) + + # Try to delete a non-existent profile + cmd = ["delete", "nonexistent"] + stdout, stderr, exit_code = self.run_command(cmd) + self.assertEqual(exit_code, 1) + self.assertIn("does not exist", stdout) + + +if __name__ == "__main__": + unittest.main()