diff --git a/docs/tech-specs/architecture_principles.md b/docs/tech-specs/architecture-principles.md similarity index 100% rename from docs/tech-specs/architecture_principles.md rename to docs/tech-specs/architecture-principles.md diff --git a/docs/tech-specs/logging_strategy.md b/docs/tech-specs/logging-strategy.md similarity index 100% rename from docs/tech-specs/logging_strategy.md rename to docs/tech-specs/logging-strategy.md diff --git a/docs/tech-specs/mcp_tool_arguments.md b/docs/tech-specs/mcp-tool-arguments.md similarity index 100% rename from docs/tech-specs/mcp_tool_arguments.md rename to docs/tech-specs/mcp-tool-arguments.md diff --git a/docs/tech-specs/more-config-cli.md b/docs/tech-specs/more-config-cli.md new file mode 100644 index 00000000..6cae9e13 --- /dev/null +++ b/docs/tech-specs/more-config-cli.md @@ -0,0 +1,279 @@ +# More Configuration CLI Technical Specification + +## Overview + +This specification describes enhanced command-line configuration capabilities for TrustGraph, enabling users to manage individual configuration items through granular CLI commands. The integration supports four primary use cases: + +1. **List Configuration Items**: Display configuration keys of a specific type +2. **Get Configuration Item**: Retrieve specific configuration values +3. **Put Configuration Item**: Set or update individual configuration items +4. **Delete Configuration Item**: Remove specific configuration items + +## Goals + +- **Granular Control**: Enable management of individual configuration items rather than bulk operations +- **Type-Based Listing**: Allow users to explore configuration items by type +- **Single Item Operations**: Provide commands for get/put/delete of individual config items +- **API Integration**: Leverage existing Config API for all operations +- **Consistent CLI Pattern**: Follow established TrustGraph CLI conventions and patterns +- **Error Handling**: Provide clear error messages for invalid operations +- **JSON Output**: Support structured output for programmatic use +- **Documentation**: Include comprehensive help and usage examples + +## Background + +TrustGraph currently provides configuration management through the Config API and a single CLI command `tg-show-config` that displays the entire configuration. While this works for viewing configuration, it lacks granular management capabilities. + +Current limitations include: +- No way to list configuration items by type from CLI +- No CLI command to retrieve specific configuration values +- No CLI command to set individual configuration items +- No CLI command to delete specific configuration items + +This specification addresses these gaps by adding four new CLI commands that provide granular configuration management. By exposing individual Config API operations through CLI commands, TrustGraph can: +- Enable scripted configuration management +- Allow exploration of configuration structure by type +- Support targeted configuration updates +- Provide fine-grained configuration control + +## Technical Design + +### Architecture + +The enhanced CLI configuration requires the following technical components: + +1. **tg-list-config-items** + - Lists configuration keys for a specified type + - Calls Config.list(type) API method + - Outputs list of configuration keys + + Module: `trustgraph.cli.list_config_items` + +2. **tg-get-config-item** + - Retrieves specific configuration item(s) + - Calls Config.get(keys) API method + - Outputs configuration values in JSON format + + Module: `trustgraph.cli.get_config_item` + +3. **tg-put-config-item** + - Sets or updates a configuration item + - Calls Config.put(values) API method + - Accepts type, key, and value parameters + + Module: `trustgraph.cli.put_config_item` + +4. **tg-delete-config-item** + - Removes a configuration item + - Calls Config.delete(keys) API method + - Accepts type and key parameters + + Module: `trustgraph.cli.delete_config_item` + +### Data Models + +#### ConfigKey and ConfigValue + +The commands utilize existing data structures from `trustgraph.api.types`: + +```python +@dataclasses.dataclass +class ConfigKey: + type : str + key : str + +@dataclasses.dataclass +class ConfigValue: + type : str + key : str + value : str +``` + +This approach allows: +- Consistent data handling across CLI and API +- Type-safe configuration operations +- Structured input/output formats +- Integration with existing Config API + +### CLI Command Specifications + +#### tg-list-config-items +```bash +tg-list-config-items --type [--format text|json] [--api-url ] +``` +- **Purpose**: List all configuration keys for a given type +- **API Call**: `Config.list(type)` +- **Output**: + - `text` (default): Configuration keys separated by newlines + - `json`: JSON array of configuration keys + +#### tg-get-config-item +```bash +tg-get-config-item --type --key [--format text|json] [--api-url ] +``` +- **Purpose**: Retrieve specific configuration item +- **API Call**: `Config.get([ConfigKey(type, key)])` +- **Output**: + - `text` (default): Raw string value + - `json`: JSON-encoded string value + +#### tg-put-config-item +```bash +tg-put-config-item --type --key --value [--api-url ] +tg-put-config-item --type --key --stdin [--api-url ] +``` +- **Purpose**: Set or update configuration item +- **API Call**: `Config.put([ConfigValue(type, key, value)])` +- **Input Options**: + - `--value`: String value provided directly on command line + - `--stdin`: Read value from standard input +- **Output**: Success confirmation + +#### tg-delete-config-item +```bash +tg-delete-config-item --type --key [--api-url ] +``` +- **Purpose**: Delete configuration item +- **API Call**: `Config.delete([ConfigKey(type, key)])` +- **Output**: Success confirmation + +### Implementation Details + +All commands follow the established TrustGraph CLI pattern: +- Use `argparse` for command-line argument parsing +- Import and use `trustgraph.api.Api` for backend communication +- Follow the same error handling patterns as existing CLI commands +- Support the standard `--api-url` parameter for API endpoint configuration +- Provide descriptive help text and usage examples + +#### Output Format Handling + +**Text Format (Default)**: +- `tg-list-config-items`: One key per line, plain text +- `tg-get-config-item`: Raw string value, no quotes or encoding + +**JSON Format**: +- `tg-list-config-items`: Array of strings `["key1", "key2", "key3"]` +- `tg-get-config-item`: JSON-encoded string value `"actual string value"` + +#### Input Handling + +**tg-put-config-item** supports two mutually exclusive input methods: +- `--value `: Direct command-line string value +- `--stdin`: Read entire input from standard input as the configuration value +- stdin contents are read as raw text (preserving newlines, whitespace, etc.) +- Supports piping from files, commands, or interactive input + +## Security Considerations + +- **Input Validation**: All command-line parameters must be validated before API calls +- **API Authentication**: Commands inherit existing API authentication mechanisms +- **Configuration Access**: Commands respect existing configuration access controls +- **Error Information**: Error messages should not leak sensitive configuration details + +## Performance Considerations + +- **Single Item Operations**: Commands are designed for individual items, avoiding bulk operation overhead +- **API Efficiency**: Direct API calls minimize processing layers +- **Network Latency**: Each command makes one API call, minimizing network round trips +- **Memory Usage**: Minimal memory footprint for single-item operations + +## Testing Strategy + +- **Unit Tests**: Test each CLI command module independently +- **Integration Tests**: Test CLI commands against live Config API +- **Error Handling Tests**: Verify proper error handling for invalid inputs +- **API Compatibility**: Ensure commands work with existing Config API versions + +## Migration Plan + +No migration required - these are new CLI commands that complement existing functionality: +- Existing `tg-show-config` command remains unchanged +- New commands can be added incrementally +- No breaking changes to existing configuration workflows + +## Packaging and Distribution + +These commands will be added to the existing `trustgraph-cli` package: + +**Package Location**: `trustgraph-cli/` +**Module Files**: +- `trustgraph-cli/trustgraph/cli/list_config_items.py` +- `trustgraph-cli/trustgraph/cli/get_config_item.py` +- `trustgraph-cli/trustgraph/cli/put_config_item.py` +- `trustgraph-cli/trustgraph/cli/delete_config_item.py` + +**Entry Points**: Added to `trustgraph-cli/pyproject.toml` in `[project.scripts]` section: +```toml +tg-list-config-items = "trustgraph.cli.list_config_items:main" +tg-get-config-item = "trustgraph.cli.get_config_item:main" +tg-put-config-item = "trustgraph.cli.put_config_item:main" +tg-delete-config-item = "trustgraph.cli.delete_config_item:main" +``` + +## Implementation Tasks + +1. **Create CLI Modules**: Implement the four CLI command modules in `trustgraph-cli/trustgraph/cli/` +2. **Update pyproject.toml**: Add new command entry points to `trustgraph-cli/pyproject.toml` +3. **Documentation**: Create CLI documentation for each command in `docs/cli/` +4. **Testing**: Implement comprehensive test coverage +5. **Integration**: Ensure commands work with existing TrustGraph infrastructure +6. **Package Build**: Verify commands are properly installed with `pip install trustgraph-cli` + +## Usage Examples + +#### List configuration items +```bash +# List prompt keys (text format) +tg-list-config-items --type prompt +template-1 +template-2 +system-prompt + +# List prompt keys (JSON format) +tg-list-config-items --type prompt --format json +["template-1", "template-2", "system-prompt"] +``` + +#### Get configuration item +```bash +# Get prompt value (text format) +tg-get-config-item --type prompt --key template-1 +You are a helpful assistant. Please respond to: {query} + +# Get prompt value (JSON format) +tg-get-config-item --type prompt --key template-1 --format json +"You are a helpful assistant. Please respond to: {query}" +``` + +#### Set configuration item +```bash +# Set from command line +tg-put-config-item --type prompt --key new-template --value "Custom prompt: {input}" + +# Set from file via pipe +cat ./prompt-template.txt | tg-put-config-item --type prompt --key complex-template --stdin + +# Set from file via redirect +tg-put-config-item --type prompt --key complex-template --stdin < ./prompt-template.txt + +# Set from command output +echo "Generated template: {query}" | tg-put-config-item --type prompt --key auto-template --stdin +``` + +#### Delete configuration item +```bash +tg-delete-config-item --type prompt --key old-template +``` + +## Open Questions + +- Should commands support batch operations (multiple keys) in addition to single items? +- What output format should be used for success confirmations? +- How should configuration types be documented/discovered by users? + +## References + +- Existing Config API: `trustgraph/api/config.py` +- CLI patterns: `trustgraph-cli/trustgraph/cli/show_config.py` +- Data types: `trustgraph/api/types.py` \ No newline at end of file diff --git a/docs/tech-specs/schema_refactoring_proposal.md b/docs/tech-specs/schema-refactoring-proposal.md similarity index 100% rename from docs/tech-specs/schema_refactoring_proposal.md rename to docs/tech-specs/schema-refactoring-proposal.md diff --git a/docs/tech-specs/structured_data_schemas.md b/docs/tech-specs/structured-data-schemas.md similarity index 100% rename from docs/tech-specs/structured_data_schemas.md rename to docs/tech-specs/structured-data-schemas.md diff --git a/docs/tech-specs/structured_data.md b/docs/tech-specs/structured-data.md similarity index 100% rename from docs/tech-specs/structured_data.md rename to docs/tech-specs/structured-data.md diff --git a/tests/integration/test_config_cli_integration.py b/tests/integration/test_config_cli_integration.py new file mode 100644 index 00000000..3d638103 --- /dev/null +++ b/tests/integration/test_config_cli_integration.py @@ -0,0 +1,336 @@ +""" +Integration tests for CLI configuration commands. + +Tests the full command execution flow with mocked API responses +to verify end-to-end functionality. +""" + +import pytest +import json +import sys +from unittest.mock import patch, Mock, MagicMock +from io import StringIO + +# Import the CLI modules directly for integration testing +from trustgraph.cli.list_config_items import main as list_main +from trustgraph.cli.get_config_item import main as get_main +from trustgraph.cli.put_config_item import main as put_main +from trustgraph.cli.delete_config_item import main as delete_main + + +class TestConfigCLIIntegration: + """Test CLI commands with mocked API responses.""" + + @patch('trustgraph.cli.list_config_items.Api') + def test_list_config_items_integration(self, mock_api_class, capsys): + """Test tg-list-config-items with mocked API response.""" + # Mock the API and config objects + mock_api = MagicMock() + mock_config = MagicMock() + mock_api.config.return_value = mock_config + mock_api_class.return_value = mock_api + + # Mock the list response + mock_config.list.return_value = ["template-1", "template-2", "system-prompt"] + + # Run the command with test args + test_args = [ + 'tg-list-config-items', + '--type', 'prompt', + '--format', 'json' + ] + + with patch('sys.argv', test_args): + list_main() + + captured = capsys.readouterr() + output = json.loads(captured.out.strip()) + assert output == ["template-1", "template-2", "system-prompt"] + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_config_item_integration(self, mock_api_class, capsys): + """Test tg-get-config-item with mocked API response.""" + from trustgraph.api.types import ConfigValue + + # Mock the API and config objects + mock_api = MagicMock() + mock_config = MagicMock() + mock_api.config.return_value = mock_config + mock_api_class.return_value = mock_api + + # Mock the get response + mock_config_value = ConfigValue( + type="prompt", + key="template-1", + value="You are a helpful assistant. Please respond to: {query}" + ) + mock_config.get.return_value = [mock_config_value] + + # Run the command with test args + test_args = [ + 'tg-get-config-item', + '--type', 'prompt', + '--key', 'template-1', + '--format', 'text' + ] + + with patch('sys.argv', test_args): + get_main() + + captured = capsys.readouterr() + assert captured.out.strip() == "You are a helpful assistant. Please respond to: {query}" + + @patch('trustgraph.cli.put_config_item.Api') + def test_put_config_item_integration(self, mock_api_class, capsys): + """Test tg-put-config-item with mocked API response.""" + # Mock the API and config objects + mock_api = MagicMock() + mock_config = MagicMock() + mock_api.config.return_value = mock_config + mock_api_class.return_value = mock_api + + # Run the command with test args + test_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'new-template', + '--value', 'Custom prompt: {input}' + ] + + with patch('sys.argv', test_args): + put_main() + + captured = capsys.readouterr() + assert "Configuration item set: prompt/new-template" in captured.out + + @patch('trustgraph.cli.delete_config_item.Api') + def test_delete_config_item_integration(self, mock_api_class, capsys): + """Test tg-delete-config-item with mocked API response.""" + # Mock the API and config objects + mock_api = MagicMock() + mock_config = MagicMock() + mock_api.config.return_value = mock_config + mock_api_class.return_value = mock_api + + # Run the command with test args + test_args = [ + 'tg-delete-config-item', + '--type', 'prompt', + '--key', 'old-template' + ] + + with patch('sys.argv', test_args): + delete_main() + + captured = capsys.readouterr() + assert "Configuration item deleted: prompt/old-template" in captured.out + + @patch('trustgraph.cli.put_config_item.Api') + def test_put_config_item_stdin_integration(self, mock_api_class, capsys): + """Test tg-put-config-item with stdin input.""" + # Mock the API and config objects + mock_api = MagicMock() + mock_config = MagicMock() + mock_api.config.return_value = mock_config + mock_api_class.return_value = mock_api + + stdin_content = "Multi-line template:\nLine 1\nLine 2" + + # Run the command with test args and mocked stdin + test_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'multiline-template', + '--stdin' + ] + + with patch('sys.argv', test_args), \ + patch('sys.stdin', StringIO(stdin_content)): + put_main() + + captured = capsys.readouterr() + assert "Configuration item set: prompt/multiline-template" in captured.out + + @patch('trustgraph.cli.list_config_items.Api') + def test_api_error_handling_integration(self, mock_api_class, capsys): + """Test CLI commands handle API errors gracefully.""" + # Mock API to raise an exception + mock_api_class.side_effect = Exception("Configuration type not found") + + test_args = [ + 'tg-list-config-items', + '--type', 'nonexistent' + ] + + with patch('sys.argv', test_args): + list_main() + + captured = capsys.readouterr() + assert "Exception:" in captured.out + assert "Configuration type not found" in captured.out + + def test_list_help_message(self): + """Test that help message is displayed correctly.""" + test_args = ['tg-list-config-items', '--help'] + + with patch('sys.argv', test_args): + with pytest.raises(SystemExit) as exc_info: + list_main() + # Help command exits with code 0 + assert exc_info.value.code == 0 + + def test_missing_required_args(self): + """Test that missing required arguments are handled.""" + # Test list without --type + test_args = ['tg-list-config-items'] + + with patch('sys.argv', test_args): + with pytest.raises(SystemExit) as exc_info: + list_main() + # Missing required args exit with non-zero code + assert exc_info.value.code != 0 + + # Test get without --key + test_args = ['tg-get-config-item', '--type', 'prompt'] + + with patch('sys.argv', test_args): + with pytest.raises(SystemExit) as exc_info: + get_main() + assert exc_info.value.code != 0 + + def test_mutually_exclusive_put_args(self): + """Test that --value and --stdin are mutually exclusive.""" + test_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'test', + '--value', 'test', + '--stdin' + ] + + with patch('sys.argv', test_args): + with pytest.raises(SystemExit) as exc_info: + put_main() + assert exc_info.value.code != 0 + + +class TestConfigCLIWorkflow: + """Test complete workflows using multiple commands.""" + + @patch('trustgraph.cli.put_config_item.Api') + @patch('trustgraph.cli.get_config_item.Api') + def test_put_then_get_workflow(self, mock_get_api, mock_put_api, capsys): + """Test putting a config item then retrieving it.""" + from trustgraph.api.types import ConfigValue + + # Mock put API + mock_put_config = MagicMock() + mock_put_api.return_value.config.return_value = mock_put_config + + # Mock get API + mock_get_config = MagicMock() + mock_get_api.return_value.config.return_value = mock_get_config + mock_config_value = ConfigValue( + type="prompt", + key="workflow-test", + value="Workflow test value" + ) + mock_get_config.get.return_value = [mock_config_value] + + # Put config item + put_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'workflow-test', + '--value', 'Workflow test value' + ] + + with patch('sys.argv', put_args): + put_main() + + put_output = capsys.readouterr() + assert "Configuration item set" in put_output.out + + # Get config item + get_args = [ + 'tg-get-config-item', + '--type', 'prompt', + '--key', 'workflow-test' + ] + + with patch('sys.argv', get_args): + get_main() + + get_output = capsys.readouterr() + assert get_output.out.strip() == "Workflow test value" + + @patch('trustgraph.cli.list_config_items.Api') + @patch('trustgraph.cli.put_config_item.Api') + @patch('trustgraph.cli.delete_config_item.Api') + def test_list_put_delete_workflow(self, mock_delete_api, mock_put_api, mock_list_api, capsys): + """Test list, put, then delete workflow.""" + # Mock list API (empty initially, then with item) + mock_list_config = MagicMock() + mock_list_api.return_value.config.return_value = mock_list_config + mock_list_config.list.side_effect = [[], ["new-item"]] # Empty first, then has item + + # Mock put API + mock_put_config = MagicMock() + mock_put_api.return_value.config.return_value = mock_put_config + + # Mock delete API + mock_delete_config = MagicMock() + mock_delete_api.return_value.config.return_value = mock_delete_config + + # List (should be empty) + list_args1 = [ + 'tg-list-config-items', + '--type', 'prompt', + '--format', 'json' + ] + + with patch('sys.argv', list_args1): + list_main() + + list_output1 = capsys.readouterr() + assert json.loads(list_output1.out.strip()) == [] + + # Put item + put_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'new-item', + '--value', 'New item value' + ] + + with patch('sys.argv', put_args): + put_main() + + put_output = capsys.readouterr() + assert "Configuration item set" in put_output.out + + # List (should contain new item) + list_args2 = [ + 'tg-list-config-items', + '--type', 'prompt', + '--format', 'json' + ] + + with patch('sys.argv', list_args2): + list_main() + + list_output2 = capsys.readouterr() + assert json.loads(list_output2.out.strip()) == ["new-item"] + + # Delete item + delete_args = [ + 'tg-delete-config-item', + '--type', 'prompt', + '--key', 'new-item' + ] + + with patch('sys.argv', delete_args): + delete_main() + + delete_output = capsys.readouterr() + assert "Configuration item deleted" in delete_output.out \ No newline at end of file diff --git a/tests/unit/test_cli/test_config_commands.py b/tests/unit/test_cli/test_config_commands.py new file mode 100644 index 00000000..286054b9 --- /dev/null +++ b/tests/unit/test_cli/test_config_commands.py @@ -0,0 +1,458 @@ +""" +Unit tests for CLI configuration commands. + +Tests the business logic of list/get/put/delete config item commands +while mocking the Config API. +""" + +import pytest +import json +import sys +from unittest.mock import Mock, patch, MagicMock +from io import StringIO + +from trustgraph.cli.list_config_items import list_config_items, main as list_main +from trustgraph.cli.get_config_item import get_config_item, main as get_main +from trustgraph.cli.put_config_item import put_config_item, main as put_main +from trustgraph.cli.delete_config_item import delete_config_item, main as delete_main +from trustgraph.api.types import ConfigKey, ConfigValue + + +@pytest.fixture +def mock_api(): + """Mock Api instance with config() method.""" + mock_api_instance = Mock() + mock_config = Mock() + mock_api_instance.config.return_value = mock_config + return mock_api_instance, mock_config + + +@pytest.fixture +def sample_config_keys(): + """Sample configuration keys.""" + return ["template-1", "template-2", "system-prompt"] + + +@pytest.fixture +def sample_config_value(): + """Sample configuration value.""" + return ConfigValue( + type="prompt", + key="template-1", + value="You are a helpful assistant. Please respond to: {query}" + ) + + +class TestListConfigItems: + """Test the list_config_items function.""" + + @patch('trustgraph.cli.list_config_items.Api') + def test_list_config_items_text_format(self, mock_api_class, mock_api, sample_config_keys, capsys): + """Test listing config items in text format.""" + mock_api_class.return_value, mock_config = mock_api + mock_config.list.return_value = sample_config_keys + + list_config_items("http://test.com", "prompt", "text") + + captured = capsys.readouterr() + output_lines = captured.out.strip().split('\n') + + assert len(output_lines) == 3 + assert "template-1" in output_lines + assert "template-2" in output_lines + assert "system-prompt" in output_lines + + mock_config.list.assert_called_once_with("prompt") + + @patch('trustgraph.cli.list_config_items.Api') + def test_list_config_items_json_format(self, mock_api_class, mock_api, sample_config_keys, capsys): + """Test listing config items in JSON format.""" + mock_api_class.return_value, mock_config = mock_api + mock_config.list.return_value = sample_config_keys + + list_config_items("http://test.com", "prompt", "json") + + captured = capsys.readouterr() + output = json.loads(captured.out.strip()) + + assert output == sample_config_keys + mock_config.list.assert_called_once_with("prompt") + + @patch('trustgraph.cli.list_config_items.Api') + def test_list_config_items_empty_list(self, mock_api_class, mock_api, capsys): + """Test listing when no config items exist.""" + mock_api_class.return_value, mock_config = mock_api + mock_config.list.return_value = [] + + list_config_items("http://test.com", "nonexistent", "text") + + captured = capsys.readouterr() + assert captured.out.strip() == "" + + mock_config.list.assert_called_once_with("nonexistent") + + def test_list_main_parses_args_correctly(self): + """Test that list main() parses arguments correctly.""" + test_args = [ + 'tg-list-config-items', + '--type', 'prompt', + '--format', 'json', + '--api-url', 'http://custom.com' + ] + + with patch('sys.argv', test_args), \ + patch('trustgraph.cli.list_config_items.list_config_items') as mock_list: + + list_main() + + mock_list.assert_called_once_with( + url='http://custom.com', + config_type='prompt', + format_type='json' + ) + + def test_list_main_uses_defaults(self): + """Test that list main() uses default values.""" + test_args = [ + 'tg-list-config-items', + '--type', 'prompt' + ] + + with patch('sys.argv', test_args), \ + patch('trustgraph.cli.list_config_items.list_config_items') as mock_list: + + list_main() + + mock_list.assert_called_once_with( + url='http://localhost:8088/', + config_type='prompt', + format_type='text' + ) + + +class TestGetConfigItem: + """Test the get_config_item function.""" + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_config_item_text_format(self, mock_api_class, mock_api, sample_config_value, capsys): + """Test getting config item in text format.""" + mock_api_class.return_value, mock_config = mock_api + mock_config.get.return_value = [sample_config_value] + + get_config_item("http://test.com", "prompt", "template-1", "text") + + captured = capsys.readouterr() + assert captured.out.strip() == sample_config_value.value + + # Verify ConfigKey was constructed correctly + call_args = mock_config.get.call_args[0][0] + assert len(call_args) == 1 + config_key = call_args[0] + assert config_key.type == "prompt" + assert config_key.key == "template-1" + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_config_item_json_format(self, mock_api_class, mock_api, sample_config_value, capsys): + """Test getting config item in JSON format.""" + mock_api_class.return_value, mock_config = mock_api + mock_config.get.return_value = [sample_config_value] + + get_config_item("http://test.com", "prompt", "template-1", "json") + + captured = capsys.readouterr() + output = json.loads(captured.out.strip()) + + assert output == sample_config_value.value + mock_config.get.assert_called_once() + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_config_item_not_found(self, mock_api_class, mock_api): + """Test getting non-existent config item raises exception.""" + mock_api_class.return_value, mock_config = mock_api + mock_config.get.return_value = [] + + with pytest.raises(Exception, match="Configuration item not found"): + get_config_item("http://test.com", "prompt", "nonexistent", "text") + + def test_get_main_parses_args_correctly(self): + """Test that get main() parses arguments correctly.""" + test_args = [ + 'tg-get-config-item', + '--type', 'prompt', + '--key', 'template-1', + '--format', 'json', + '--api-url', 'http://custom.com' + ] + + with patch('sys.argv', test_args), \ + patch('trustgraph.cli.get_config_item.get_config_item') as mock_get: + + get_main() + + mock_get.assert_called_once_with( + url='http://custom.com', + config_type='prompt', + key='template-1', + format_type='json' + ) + + +class TestPutConfigItem: + """Test the put_config_item function.""" + + @patch('trustgraph.cli.put_config_item.Api') + def test_put_config_item_with_value(self, mock_api_class, mock_api, capsys): + """Test putting config item with command line value.""" + mock_api_class.return_value, mock_config = mock_api + + put_config_item("http://test.com", "prompt", "new-template", "Custom prompt: {input}") + + captured = capsys.readouterr() + assert "Configuration item set: prompt/new-template" in captured.out + + # Verify ConfigValue was constructed correctly + call_args = mock_config.put.call_args[0][0] + assert len(call_args) == 1 + config_value = call_args[0] + assert config_value.type == "prompt" + assert config_value.key == "new-template" + assert config_value.value == "Custom prompt: {input}" + + @patch('trustgraph.cli.put_config_item.Api') + def test_put_config_item_multiline_value(self, mock_api_class, mock_api): + """Test putting config item with multiline value.""" + mock_api_class.return_value, mock_config = mock_api + + multiline_value = "Line 1\nLine 2\nLine 3" + put_config_item("http://test.com", "prompt", "multiline-template", multiline_value) + + call_args = mock_config.put.call_args[0][0] + config_value = call_args[0] + assert config_value.value == multiline_value + + def test_put_main_with_value_arg(self): + """Test put main() with --value argument.""" + test_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'new-template', + '--value', 'Custom prompt: {input}', + '--api-url', 'http://custom.com' + ] + + with patch('sys.argv', test_args), \ + patch('trustgraph.cli.put_config_item.put_config_item') as mock_put: + + put_main() + + mock_put.assert_called_once_with( + url='http://custom.com', + config_type='prompt', + key='new-template', + value='Custom prompt: {input}' + ) + + def test_put_main_with_stdin_arg(self): + """Test put main() with --stdin argument.""" + test_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'stdin-template', + '--stdin' + ] + + stdin_content = "Content from stdin\nMultiple lines" + + with patch('sys.argv', test_args), \ + patch('sys.stdin', StringIO(stdin_content)), \ + patch('trustgraph.cli.put_config_item.put_config_item') as mock_put: + + put_main() + + mock_put.assert_called_once_with( + url='http://localhost:8088/', + config_type='prompt', + key='stdin-template', + value=stdin_content + ) + + def test_put_main_mutually_exclusive_args(self): + """Test that --value and --stdin are mutually exclusive.""" + test_args = [ + 'tg-put-config-item', + '--type', 'prompt', + '--key', 'template', + '--value', 'test', + '--stdin' + ] + + with patch('sys.argv', test_args): + with pytest.raises(SystemExit): + put_main() + + +class TestDeleteConfigItem: + """Test the delete_config_item function.""" + + @patch('trustgraph.cli.delete_config_item.Api') + def test_delete_config_item(self, mock_api_class, mock_api, capsys): + """Test deleting config item.""" + mock_api_class.return_value, mock_config = mock_api + + delete_config_item("http://test.com", "prompt", "old-template") + + captured = capsys.readouterr() + assert "Configuration item deleted: prompt/old-template" in captured.out + + # Verify ConfigKey was constructed correctly + call_args = mock_config.delete.call_args[0][0] + assert len(call_args) == 1 + config_key = call_args[0] + assert config_key.type == "prompt" + assert config_key.key == "old-template" + + def test_delete_main_parses_args_correctly(self): + """Test that delete main() parses arguments correctly.""" + test_args = [ + 'tg-delete-config-item', + '--type', 'prompt', + '--key', 'old-template', + '--api-url', 'http://custom.com' + ] + + with patch('sys.argv', test_args), \ + patch('trustgraph.cli.delete_config_item.delete_config_item') as mock_delete: + + delete_main() + + mock_delete.assert_called_once_with( + url='http://custom.com', + config_type='prompt', + key='old-template' + ) + + +class TestErrorHandling: + """Test error handling scenarios.""" + + @patch('trustgraph.cli.list_config_items.Api') + def test_list_handles_api_exception(self, mock_api_class, capsys): + """Test that list command handles API exceptions.""" + mock_api_class.side_effect = Exception("API connection failed") + + list_main_with_args(['--type', 'prompt']) + + captured = capsys.readouterr() + assert "Exception: API connection failed" in captured.out + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_handles_api_exception(self, mock_api_class, capsys): + """Test that get command handles API exceptions.""" + mock_api_class.side_effect = Exception("API connection failed") + + get_main_with_args(['--type', 'prompt', '--key', 'test']) + + captured = capsys.readouterr() + assert "Exception: API connection failed" in captured.out + + @patch('trustgraph.cli.put_config_item.Api') + def test_put_handles_api_exception(self, mock_api_class, capsys): + """Test that put command handles API exceptions.""" + mock_api_class.side_effect = Exception("API connection failed") + + put_main_with_args(['--type', 'prompt', '--key', 'test', '--value', 'test']) + + captured = capsys.readouterr() + assert "Exception: API connection failed" in captured.out + + @patch('trustgraph.cli.delete_config_item.Api') + def test_delete_handles_api_exception(self, mock_api_class, capsys): + """Test that delete command handles API exceptions.""" + mock_api_class.side_effect = Exception("API connection failed") + + delete_main_with_args(['--type', 'prompt', '--key', 'test']) + + captured = capsys.readouterr() + assert "Exception: API connection failed" in captured.out + + +class TestDataValidation: + """Test data validation and edge cases.""" + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_empty_string_value(self, mock_api_class, mock_api, capsys): + """Test getting config item with empty string value.""" + mock_api_class.return_value, mock_config = mock_api + empty_value = ConfigValue(type="prompt", key="empty", value="") + mock_config.get.return_value = [empty_value] + + get_config_item("http://test.com", "prompt", "empty", "text") + + captured = capsys.readouterr() + assert captured.out == "\n" # Just a newline from print() + + @patch('trustgraph.cli.put_config_item.Api') + def test_put_empty_string_value(self, mock_api_class, mock_api): + """Test putting config item with empty string value.""" + mock_api_class.return_value, mock_config = mock_api + + put_config_item("http://test.com", "prompt", "empty", "") + + call_args = mock_config.put.call_args[0][0] + config_value = call_args[0] + assert config_value.value == "" + + @patch('trustgraph.cli.get_config_item.Api') + def test_get_special_characters_value(self, mock_api_class, mock_api, capsys): + """Test getting config item with special characters.""" + mock_api_class.return_value, mock_config = mock_api + special_value = ConfigValue( + type="prompt", + key="special", + value="Special chars: äöü 中文 🌟 \"quotes\" 'apostrophes'" + ) + mock_config.get.return_value = [special_value] + + get_config_item("http://test.com", "prompt", "special", "text") + + captured = capsys.readouterr() + assert "äöü 中文 🌟" in captured.out + assert '"quotes"' in captured.out + + +# Helper functions for testing main() with custom args +def list_main_with_args(args): + """Helper to test list_main with custom arguments.""" + test_args = ['tg-list-config-items'] + args + with patch('sys.argv', test_args): + try: + list_main() + except SystemExit: + pass + +def get_main_with_args(args): + """Helper to test get_main with custom arguments.""" + test_args = ['tg-get-config-item'] + args + with patch('sys.argv', test_args): + try: + get_main() + except SystemExit: + pass + +def put_main_with_args(args): + """Helper to test put_main with custom arguments.""" + test_args = ['tg-put-config-item'] + args + with patch('sys.argv', test_args): + try: + put_main() + except SystemExit: + pass + +def delete_main_with_args(args): + """Helper to test delete_main with custom arguments.""" + test_args = ['tg-delete-config-item'] + args + with patch('sys.argv', test_args): + try: + delete_main() + except SystemExit: + pass \ No newline at end of file diff --git a/trustgraph-cli/pyproject.toml b/trustgraph-cli/pyproject.toml index 02b8d958..c8fdf0e5 100644 --- a/trustgraph-cli/pyproject.toml +++ b/trustgraph-cli/pyproject.toml @@ -78,6 +78,10 @@ tg-unload-kg-core = "trustgraph.cli.unload_kg_core:main" tg-start-library-processing = "trustgraph.cli.start_library_processing:main" tg-stop-flow = "trustgraph.cli.stop_flow:main" tg-stop-library-processing = "trustgraph.cli.stop_library_processing:main" +tg-list-config-items = "trustgraph.cli.list_config_items:main" +tg-get-config-item = "trustgraph.cli.get_config_item:main" +tg-put-config-item = "trustgraph.cli.put_config_item:main" +tg-delete-config-item = "trustgraph.cli.delete_config_item:main" [tool.setuptools.packages.find] include = ["trustgraph*"] diff --git a/trustgraph-cli/trustgraph/cli/delete_config_item.py b/trustgraph-cli/trustgraph/cli/delete_config_item.py new file mode 100644 index 00000000..1de02890 --- /dev/null +++ b/trustgraph-cli/trustgraph/cli/delete_config_item.py @@ -0,0 +1,61 @@ +""" +Deletes a configuration item +""" + +import argparse +import os +from trustgraph.api import Api +from trustgraph.api.types import ConfigKey + +default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/') + +def delete_config_item(url, config_type, key): + + api = Api(url).config() + + config_key = ConfigKey(type=config_type, key=key) + api.delete([config_key]) + + print(f"Configuration item deleted: {config_type}/{key}") + +def main(): + + parser = argparse.ArgumentParser( + prog='tg-delete-config-item', + description=__doc__, + ) + + parser.add_argument( + '--type', + required=True, + help='Configuration type', + ) + + parser.add_argument( + '--key', + required=True, + help='Configuration key', + ) + + parser.add_argument( + '-u', '--api-url', + default=default_url, + help=f'API URL (default: {default_url})', + ) + + args = parser.parse_args() + + try: + + delete_config_item( + url=args.api_url, + config_type=args.type, + key=args.key, + ) + + except Exception as e: + + print("Exception:", e, flush=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/trustgraph-cli/trustgraph/cli/get_config_item.py b/trustgraph-cli/trustgraph/cli/get_config_item.py new file mode 100644 index 00000000..832d2711 --- /dev/null +++ b/trustgraph-cli/trustgraph/cli/get_config_item.py @@ -0,0 +1,78 @@ +""" +Gets a specific configuration item +""" + +import argparse +import os +import json +from trustgraph.api import Api +from trustgraph.api.types import ConfigKey + +default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/') + +def get_config_item(url, config_type, key, format_type): + + api = Api(url).config() + + config_key = ConfigKey(type=config_type, key=key) + values = api.get([config_key]) + + if not values: + raise Exception(f"Configuration item not found: {config_type}/{key}") + + value = values[0].value + + if format_type == "json": + print(json.dumps(value)) + else: + print(value) + +def main(): + + parser = argparse.ArgumentParser( + prog='tg-get-config-item', + description=__doc__, + ) + + parser.add_argument( + '--type', + required=True, + help='Configuration type', + ) + + parser.add_argument( + '--key', + required=True, + help='Configuration key', + ) + + parser.add_argument( + '--format', + choices=['text', 'json'], + default='text', + help='Output format (default: text)', + ) + + parser.add_argument( + '-u', '--api-url', + default=default_url, + help=f'API URL (default: {default_url})', + ) + + args = parser.parse_args() + + try: + + get_config_item( + url=args.api_url, + config_type=args.type, + key=args.key, + format_type=args.format, + ) + + except Exception as e: + + print("Exception:", e, flush=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/trustgraph-cli/trustgraph/cli/list_config_items.py b/trustgraph-cli/trustgraph/cli/list_config_items.py new file mode 100644 index 00000000..33e8f7ba --- /dev/null +++ b/trustgraph-cli/trustgraph/cli/list_config_items.py @@ -0,0 +1,65 @@ +""" +Lists configuration items for a specified type +""" + +import argparse +import os +import json +from trustgraph.api import Api + +default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/') + +def list_config_items(url, config_type, format_type): + + api = Api(url).config() + + keys = api.list(config_type) + + if format_type == "json": + print(json.dumps(keys)) + else: + for key in keys: + print(key) + +def main(): + + parser = argparse.ArgumentParser( + prog='tg-list-config-items', + description=__doc__, + ) + + parser.add_argument( + '--type', + required=True, + help='Configuration type to list', + ) + + parser.add_argument( + '--format', + choices=['text', 'json'], + default='text', + help='Output format (default: text)', + ) + + parser.add_argument( + '-u', '--api-url', + default=default_url, + help=f'API URL (default: {default_url})', + ) + + args = parser.parse_args() + + try: + + list_config_items( + url=args.api_url, + config_type=args.type, + format_type=args.format, + ) + + except Exception as e: + + print("Exception:", e, flush=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/trustgraph-cli/trustgraph/cli/put_config_item.py b/trustgraph-cli/trustgraph/cli/put_config_item.py new file mode 100644 index 00000000..d48e29a7 --- /dev/null +++ b/trustgraph-cli/trustgraph/cli/put_config_item.py @@ -0,0 +1,80 @@ +""" +Sets a configuration item +""" + +import argparse +import os +import sys +from trustgraph.api import Api +from trustgraph.api.types import ConfigValue + +default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/') + +def put_config_item(url, config_type, key, value): + + api = Api(url).config() + + config_value = ConfigValue(type=config_type, key=key, value=value) + api.put([config_value]) + + print(f"Configuration item set: {config_type}/{key}") + +def main(): + + parser = argparse.ArgumentParser( + prog='tg-put-config-item', + description=__doc__, + ) + + parser.add_argument( + '--type', + required=True, + help='Configuration type', + ) + + parser.add_argument( + '--key', + required=True, + help='Configuration key', + ) + + value_group = parser.add_mutually_exclusive_group(required=True) + value_group.add_argument( + '--value', + help='Configuration value', + ) + + value_group.add_argument( + '--stdin', + action='store_true', + help='Read configuration value from standard input', + ) + + parser.add_argument( + '-u', '--api-url', + default=default_url, + help=f'API URL (default: {default_url})', + ) + + args = parser.parse_args() + + try: + + if args.stdin: + value = sys.stdin.read() + else: + value = args.value + + put_config_item( + url=args.api_url, + config_type=args.type, + key=args.key, + value=value, + ) + + except Exception as e: + + print("Exception:", e, flush=True) + +if __name__ == "__main__": + main() \ No newline at end of file