More config cli (#466)

* Extra config CLI tech spec

* Describe packaging

* Added CLI commands

* Add tests
This commit is contained in:
cybermaggedon 2025-08-22 13:36:10 +01:00 committed by GitHub
parent 5e71d0cadb
commit 28190fea8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1361 additions and 0 deletions

View file

@ -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 <config-type> [--format text|json] [--api-url <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 <type> --key <key> [--format text|json] [--api-url <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 <type> --key <key> --value <value> [--api-url <url>]
tg-put-config-item --type <type> --key <key> --stdin [--api-url <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 <type> --key <key> [--api-url <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 <string>`: 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`

View file

@ -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

View file

@ -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

View file

@ -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*"]

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()