tlnw-tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tlnw_tools-0.1.0/PKG-INFO +70 -0
- tlnw_tools-0.1.0/README.md +58 -0
- tlnw_tools-0.1.0/pyproject.toml +32 -0
- tlnw_tools-0.1.0/setup.cfg +4 -0
- tlnw_tools-0.1.0/tests/test_loader.py +71 -0
- tlnw_tools-0.1.0/tlnw_tools/__init__.py +2 -0
- tlnw_tools-0.1.0/tlnw_tools/cli.py +228 -0
- tlnw_tools-0.1.0/tlnw_tools/tools.yml +9 -0
- tlnw_tools-0.1.0/tlnw_tools.egg-info/PKG-INFO +70 -0
- tlnw_tools-0.1.0/tlnw_tools.egg-info/SOURCES.txt +12 -0
- tlnw_tools-0.1.0/tlnw_tools.egg-info/dependency_links.txt +1 -0
- tlnw_tools-0.1.0/tlnw_tools.egg-info/entry_points.txt +2 -0
- tlnw_tools-0.1.0/tlnw_tools.egg-info/requires.txt +1 -0
- tlnw_tools-0.1.0/tlnw_tools.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tlnw-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A collection of tools for various tasks and functionalities in the Tellers Network ecosystem.
|
|
5
|
+
Author-email: Tellers Network <admin@tlnw.uk>
|
|
6
|
+
Project-URL: Homepage, https://github.com/tellersnetwork/tlnw-tools
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: PyYAML>=6.0
|
|
12
|
+
|
|
13
|
+
# Tellers Network Tools CLI (`tlnw-tools`)
|
|
14
|
+
|
|
15
|
+
`tlnw-tools` is a modular, dynamic command-line interface (CLI) to manage and run a collection of independent tools in the Tellers Network ecosystem. Each tool is developed as a separate package published to PyPI and registered in `tools.yml`. When invoked, the main CLI dynamically verifies, downloads (if missing), and executes the tool either in-process (by default) or as a subprocess.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
- **Dynamic Tool Execution**: Run tools registered in `tools.yml` directly using `tlnw-tools <tool>`.
|
|
19
|
+
- **Automatic Packaging Management**: Automatically downloads, installs, or updates tools dynamically via `pip` on invocation.
|
|
20
|
+
- **Configurable Isolation**: Supports executing tools either via in-process module importing or subprocess delegation, configurable on a per-tool basis in `tools.yml`.
|
|
21
|
+
- **Consistent CLI**: Standard common arguments like `--help` and `--debug`.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
You can install the main CLI via PyPI:
|
|
25
|
+
```bash
|
|
26
|
+
pip install tlnw-tools
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration (`tools.yml`)
|
|
30
|
+
The main CLI registry lies in `tools.yml` located in the root of the project (or can be customized):
|
|
31
|
+
```yaml
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
description: "A collection of tools for various tasks and functionalities in the Tellers Network ecosystem."
|
|
34
|
+
tools:
|
|
35
|
+
- name: "generate-image"
|
|
36
|
+
package: "tlnw-generate-image"
|
|
37
|
+
execution_mode: "inprocess_import"
|
|
38
|
+
module: "tlnw_generate_image.cli"
|
|
39
|
+
entry_point: "main"
|
|
40
|
+
description: "Generate images and illustrations with OpenAI and Gemini models."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
```bash
|
|
45
|
+
# General help
|
|
46
|
+
tlnw-tools --help
|
|
47
|
+
|
|
48
|
+
# Running a registered tool (e.g., generate-image)
|
|
49
|
+
tlnw-tools generate-image --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Tool: `generate-image`
|
|
55
|
+
|
|
56
|
+
`generate-image` is the first unified tool package. It integrates and harmonizes image generation capabilities for story folders and specific scenes across both OpenAI (DALL-E 2/3, `gpt-image-1.5`) and Google Imagen 4.
|
|
57
|
+
|
|
58
|
+
### Usage
|
|
59
|
+
```bash
|
|
60
|
+
tlnw-tools generate-image <story_folders> [options]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Options
|
|
64
|
+
- `--model <model>`: Specify model to use. Defaults to `gpt-image-1.5`. Available models: `dall-e-3`, `gpt-image-1.5`, `imagen-4`, etc.
|
|
65
|
+
- `--quality <quality>`: Normalize quality across OpenAI and Gemini. Choices: `low`, `medium`, `high`, `standard`, `hd`, `auto`.
|
|
66
|
+
- `--landscape` / `--portrait`: Control dimensions. Defaults to square (1024x1024).
|
|
67
|
+
- `--size`: Manually specify dimensions (e.g., `1024x1024`).
|
|
68
|
+
- `--reference`: Supply reference images.
|
|
69
|
+
- `--output-prefix`: Prepend a custom prefix to generated files.
|
|
70
|
+
- `--skip-update`: Avoid updating the Jekyll/Hugo `index.md` frontmatter.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Tellers Network Tools CLI (`tlnw-tools`)
|
|
2
|
+
|
|
3
|
+
`tlnw-tools` is a modular, dynamic command-line interface (CLI) to manage and run a collection of independent tools in the Tellers Network ecosystem. Each tool is developed as a separate package published to PyPI and registered in `tools.yml`. When invoked, the main CLI dynamically verifies, downloads (if missing), and executes the tool either in-process (by default) or as a subprocess.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Dynamic Tool Execution**: Run tools registered in `tools.yml` directly using `tlnw-tools <tool>`.
|
|
7
|
+
- **Automatic Packaging Management**: Automatically downloads, installs, or updates tools dynamically via `pip` on invocation.
|
|
8
|
+
- **Configurable Isolation**: Supports executing tools either via in-process module importing or subprocess delegation, configurable on a per-tool basis in `tools.yml`.
|
|
9
|
+
- **Consistent CLI**: Standard common arguments like `--help` and `--debug`.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
You can install the main CLI via PyPI:
|
|
13
|
+
```bash
|
|
14
|
+
pip install tlnw-tools
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration (`tools.yml`)
|
|
18
|
+
The main CLI registry lies in `tools.yml` located in the root of the project (or can be customized):
|
|
19
|
+
```yaml
|
|
20
|
+
version: 0.1.0
|
|
21
|
+
description: "A collection of tools for various tasks and functionalities in the Tellers Network ecosystem."
|
|
22
|
+
tools:
|
|
23
|
+
- name: "generate-image"
|
|
24
|
+
package: "tlnw-generate-image"
|
|
25
|
+
execution_mode: "inprocess_import"
|
|
26
|
+
module: "tlnw_generate_image.cli"
|
|
27
|
+
entry_point: "main"
|
|
28
|
+
description: "Generate images and illustrations with OpenAI and Gemini models."
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Commands
|
|
32
|
+
```bash
|
|
33
|
+
# General help
|
|
34
|
+
tlnw-tools --help
|
|
35
|
+
|
|
36
|
+
# Running a registered tool (e.g., generate-image)
|
|
37
|
+
tlnw-tools generate-image --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# Tool: `generate-image`
|
|
43
|
+
|
|
44
|
+
`generate-image` is the first unified tool package. It integrates and harmonizes image generation capabilities for story folders and specific scenes across both OpenAI (DALL-E 2/3, `gpt-image-1.5`) and Google Imagen 4.
|
|
45
|
+
|
|
46
|
+
### Usage
|
|
47
|
+
```bash
|
|
48
|
+
tlnw-tools generate-image <story_folders> [options]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
- `--model <model>`: Specify model to use. Defaults to `gpt-image-1.5`. Available models: `dall-e-3`, `gpt-image-1.5`, `imagen-4`, etc.
|
|
53
|
+
- `--quality <quality>`: Normalize quality across OpenAI and Gemini. Choices: `low`, `medium`, `high`, `standard`, `hd`, `auto`.
|
|
54
|
+
- `--landscape` / `--portrait`: Control dimensions. Defaults to square (1024x1024).
|
|
55
|
+
- `--size`: Manually specify dimensions (e.g., `1024x1024`).
|
|
56
|
+
- `--reference`: Supply reference images.
|
|
57
|
+
- `--output-prefix`: Prepend a custom prefix to generated files.
|
|
58
|
+
- `--skip-update`: Avoid updating the Jekyll/Hugo `index.md` frontmatter.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tlnw-tools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A collection of tools for various tasks and functionalities in the Tellers Network ecosystem."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Tellers Network", email = "admin@tlnw.uk" }
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"PyYAML>=6.0",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/tellersnetwork/tlnw-tools"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
tlnw-tools = "tlnw_tools.cli:main"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["tlnw_tools"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
tlnw_tools = ["tools.yml"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import io
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import pytest
|
|
8
|
+
except ImportError:
|
|
9
|
+
class MockPytest:
|
|
10
|
+
class RaisesContext:
|
|
11
|
+
def __init__(self, expected_exception):
|
|
12
|
+
self.expected_exception = expected_exception
|
|
13
|
+
self.value = None
|
|
14
|
+
def __enter__(self):
|
|
15
|
+
return self
|
|
16
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
17
|
+
if exc_type is None:
|
|
18
|
+
raise AssertionError(f"Did not raise {self.expected_exception}")
|
|
19
|
+
if issubclass(exc_type, self.expected_exception):
|
|
20
|
+
self.value = exc_val
|
|
21
|
+
return True
|
|
22
|
+
return False
|
|
23
|
+
def raises(self, expected_exception):
|
|
24
|
+
return self.RaisesContext(expected_exception)
|
|
25
|
+
pytest = MockPytest()
|
|
26
|
+
from tlnw_tools.cli import find_tools_config, load_tools_config, is_package_installed, main
|
|
27
|
+
|
|
28
|
+
def test_find_tools_config():
|
|
29
|
+
config_path = find_tools_config()
|
|
30
|
+
assert config_path is not None
|
|
31
|
+
assert os.path.exists(config_path)
|
|
32
|
+
assert config_path.endswith("tools.yml")
|
|
33
|
+
|
|
34
|
+
def test_load_tools_config():
|
|
35
|
+
config = load_tools_config()
|
|
36
|
+
assert isinstance(config, dict)
|
|
37
|
+
assert "tools" in config
|
|
38
|
+
assert isinstance(config["tools"], list)
|
|
39
|
+
|
|
40
|
+
# Ensure our first tool is registered
|
|
41
|
+
generate_image_tool = next((t for t in config["tools"] if t["name"] == "generate-image"), None)
|
|
42
|
+
assert generate_image_tool is not None
|
|
43
|
+
assert generate_image_tool["package"] == "tlnw-generate-image"
|
|
44
|
+
assert generate_image_tool["execution_mode"] == "inprocess_import"
|
|
45
|
+
|
|
46
|
+
def test_is_package_installed():
|
|
47
|
+
# 'yaml' should be installed as it's a dependency
|
|
48
|
+
assert is_package_installed("yaml") is True
|
|
49
|
+
# 'sys' is built-in
|
|
50
|
+
assert is_package_installed("sys") is True
|
|
51
|
+
# A random fake package name should not be installed
|
|
52
|
+
assert is_package_installed("non_existent_fake_package_xyz123") is False
|
|
53
|
+
|
|
54
|
+
def test_cli_help_stdout():
|
|
55
|
+
with mock.patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
|
|
56
|
+
with mock.patch.object(sys, 'argv', ['tlnw-tools', '--help']):
|
|
57
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
58
|
+
main()
|
|
59
|
+
assert exc_info.value.code == 0
|
|
60
|
+
captured = mock_stdout.getvalue()
|
|
61
|
+
assert "Usage: tlnw-tools" in captured
|
|
62
|
+
assert "generate-image" in captured
|
|
63
|
+
|
|
64
|
+
def test_cli_invalid_tool():
|
|
65
|
+
with mock.patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
|
|
66
|
+
with mock.patch.object(sys, 'argv', ['tlnw-tools', 'non-existent-tool']):
|
|
67
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
68
|
+
main()
|
|
69
|
+
assert exc_info.value.code == 1
|
|
70
|
+
captured = mock_stdout.getvalue()
|
|
71
|
+
assert "Error: Tool 'non-existent-tool' not registered" in captured
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
import logging
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import subprocess
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
# Configure logging format
|
|
11
|
+
logging.basicConfig(format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
12
|
+
logger = logging.getLogger("tlnw-tools")
|
|
13
|
+
|
|
14
|
+
def find_tools_config():
|
|
15
|
+
# 1. Check current working directory
|
|
16
|
+
cwd_path = os.path.join(os.getcwd(), "tools.yml")
|
|
17
|
+
if os.path.exists(cwd_path):
|
|
18
|
+
return cwd_path
|
|
19
|
+
|
|
20
|
+
# 2. Check sibling to this module's parent (dev mode)
|
|
21
|
+
dev_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tools.yml")
|
|
22
|
+
if os.path.exists(dev_path):
|
|
23
|
+
return dev_path
|
|
24
|
+
|
|
25
|
+
# 3. Check inside the module directory itself (packaged mode)
|
|
26
|
+
pkg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tools.yml")
|
|
27
|
+
if os.path.exists(pkg_path):
|
|
28
|
+
return pkg_path
|
|
29
|
+
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def load_tools_config():
|
|
33
|
+
config_path = find_tools_config()
|
|
34
|
+
if not config_path:
|
|
35
|
+
logger.error("Configuration file 'tools.yml' not found in workspace or package.")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
40
|
+
return yaml.safe_load(f)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Failed to parse tools.yml: {e}")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
def is_package_installed(module_name):
|
|
46
|
+
if not module_name:
|
|
47
|
+
return False
|
|
48
|
+
top_level = module_name.split('.')[0]
|
|
49
|
+
try:
|
|
50
|
+
spec = importlib.util.find_spec(top_level)
|
|
51
|
+
return spec is not None
|
|
52
|
+
except Exception:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def install_package(package_name):
|
|
56
|
+
logger.info(f"Package '{package_name}' not found. Installing dynamically from PyPI...")
|
|
57
|
+
try:
|
|
58
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
|
|
59
|
+
logger.info(f"Successfully installed package '{package_name}'.")
|
|
60
|
+
return True
|
|
61
|
+
except subprocess.CalledProcessError as e:
|
|
62
|
+
logger.error(f"Failed to dynamically install package '{package_name}': {e}")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def run_inprocess(tool_info, tool_args):
|
|
66
|
+
module_name = tool_info.get("module")
|
|
67
|
+
entry_point_name = tool_info.get("entry_point", "main")
|
|
68
|
+
|
|
69
|
+
if not module_name:
|
|
70
|
+
logger.error(f"Missing 'module' entry for tool '{tool_info.get('name')}' in tools.yml.")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
# Check installation
|
|
74
|
+
if not is_package_installed(module_name):
|
|
75
|
+
package_name = tool_info.get("package")
|
|
76
|
+
if not package_name:
|
|
77
|
+
logger.error(f"Tool '{tool_info.get('name')}' is not installed, and no 'package' is specified in tools.yml to install it.")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
if not install_package(package_name):
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
# Dynamic import and execution
|
|
83
|
+
try:
|
|
84
|
+
# Clear module from sys.modules to ensure a fresh import after installation
|
|
85
|
+
if module_name in sys.modules:
|
|
86
|
+
del sys.modules[module_name]
|
|
87
|
+
|
|
88
|
+
module = importlib.import_module(module_name)
|
|
89
|
+
entry_point = getattr(module, entry_point_name)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Failed to load entry point '{entry_point_name}' from module '{module_name}': {e}")
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
# Replace sys.argv with our tool arguments
|
|
95
|
+
# argv[0] is typically the script or subcommand name
|
|
96
|
+
sys.argv = [tool_info.get("name")] + tool_args
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
return entry_point()
|
|
100
|
+
except SystemExit as se:
|
|
101
|
+
return se.code
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Error during tool execution: {e}")
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
def run_subprocess(tool_info, tool_args):
|
|
107
|
+
tool_name = tool_info.get("name")
|
|
108
|
+
package_name = tool_info.get("package")
|
|
109
|
+
module_name = tool_info.get("module")
|
|
110
|
+
|
|
111
|
+
# Check if executable / script is on path
|
|
112
|
+
executable_path = shutil.which(tool_name)
|
|
113
|
+
|
|
114
|
+
if not executable_path:
|
|
115
|
+
# If not on path, verify if installed via python module, otherwise install
|
|
116
|
+
if not is_package_installed(module_name):
|
|
117
|
+
if not package_name:
|
|
118
|
+
logger.error(f"Tool '{tool_name}' executable not found on PATH, and no 'package' specified to install.")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
if not install_package(package_name):
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
# Try finding executable again after install
|
|
124
|
+
executable_path = shutil.which(tool_name)
|
|
125
|
+
if not executable_path:
|
|
126
|
+
# Fall back to running via python -m
|
|
127
|
+
if module_name:
|
|
128
|
+
logger.debug(f"Executable '{tool_name}' not on PATH. Falling back to executing via 'python -m {module_name}'.")
|
|
129
|
+
cmd = [sys.executable, "-m", module_name] + tool_args
|
|
130
|
+
try:
|
|
131
|
+
res = subprocess.run(cmd)
|
|
132
|
+
return res.returncode
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to execute module '{module_name}' via subprocess: {e}")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
else:
|
|
137
|
+
logger.error(f"Executable '{tool_name}' not found on PATH, and no module fallback available.")
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
# If executable is found
|
|
141
|
+
logger.debug(f"Executing '{executable_path}' with args {tool_args} via subprocess.")
|
|
142
|
+
try:
|
|
143
|
+
res = subprocess.run([executable_path] + tool_args)
|
|
144
|
+
return res.returncode
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Failed to run executable '{executable_path}': {e}")
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
def print_help(config):
|
|
150
|
+
print("Usage: tlnw-tools [--debug] <tool> [args...]")
|
|
151
|
+
print("\nCommon Options:")
|
|
152
|
+
print(" -h, --help Show this help message and exit")
|
|
153
|
+
print(" --debug Enable debug-level logging")
|
|
154
|
+
print("\nAvailable Tools:")
|
|
155
|
+
|
|
156
|
+
tools = config.get("tools", [])
|
|
157
|
+
if not tools:
|
|
158
|
+
print(" (No tools registered)")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
for tool in tools:
|
|
162
|
+
name = tool.get("name", "unknown")
|
|
163
|
+
desc = tool.get("description", "No description available.")
|
|
164
|
+
print(f" {name:<15} {desc}")
|
|
165
|
+
print()
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
config = load_tools_config()
|
|
169
|
+
|
|
170
|
+
# Parse CLI arguments manually to support generic tool arguments forwarding
|
|
171
|
+
args = sys.argv[1:]
|
|
172
|
+
|
|
173
|
+
# Handle help at main level
|
|
174
|
+
if not args or "-h" in args or "--help" in args:
|
|
175
|
+
# If we have args but first arg is a tool, let the tool handle its own help
|
|
176
|
+
if args and args[0] in [t.get("name") for t in config.get("tools", [])]:
|
|
177
|
+
# This is help for a specific tool, let the tool handle it
|
|
178
|
+
pass
|
|
179
|
+
else:
|
|
180
|
+
print_help(config)
|
|
181
|
+
sys.exit(0)
|
|
182
|
+
|
|
183
|
+
# Parse debug flag
|
|
184
|
+
debug_mode = False
|
|
185
|
+
if "--debug" in args:
|
|
186
|
+
debug_mode = True
|
|
187
|
+
logger.setLevel(logging.DEBUG)
|
|
188
|
+
logging.getLogger("").setLevel(logging.DEBUG)
|
|
189
|
+
logger.debug("Debug logging enabled for tlnw-tools CLI.")
|
|
190
|
+
# Remove --debug from args so it doesn't pollute subcommand matching
|
|
191
|
+
args.remove("--debug")
|
|
192
|
+
|
|
193
|
+
if not args:
|
|
194
|
+
print_help(config)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
197
|
+
tool_name = args[0]
|
|
198
|
+
tool_args = args[1:]
|
|
199
|
+
|
|
200
|
+
# Re-append --debug if we had it, so the tool also gets it if it wants it
|
|
201
|
+
if debug_mode and "--debug" not in tool_args:
|
|
202
|
+
tool_args.append("--debug")
|
|
203
|
+
|
|
204
|
+
# Find matching tool
|
|
205
|
+
matching_tool = None
|
|
206
|
+
for tool in config.get("tools", []):
|
|
207
|
+
if tool.get("name") == tool_name:
|
|
208
|
+
matching_tool = tool
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
if not matching_tool:
|
|
212
|
+
print(f"Error: Tool '{tool_name}' not registered in tools.yml.")
|
|
213
|
+
print_help(config)
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
|
|
216
|
+
# Execute according to mode
|
|
217
|
+
mode = matching_tool.get("execution_mode", "inprocess_import")
|
|
218
|
+
logger.debug(f"Executing tool '{tool_name}' in '{mode}' mode.")
|
|
219
|
+
|
|
220
|
+
if mode == "subprocess":
|
|
221
|
+
exit_code = run_subprocess(matching_tool, tool_args)
|
|
222
|
+
else:
|
|
223
|
+
exit_code = run_inprocess(matching_tool, tool_args)
|
|
224
|
+
|
|
225
|
+
sys.exit(exit_code or 0)
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
version: "0.1.0"
|
|
2
|
+
description: "A collection of tools for various tasks and functionalities in the Tellers Network ecosystem."
|
|
3
|
+
tools:
|
|
4
|
+
- name: "generate-image"
|
|
5
|
+
package: "tlnw-generate-image"
|
|
6
|
+
execution_mode: "inprocess_import"
|
|
7
|
+
module: "tlnw_generate_image.cli"
|
|
8
|
+
entry_point: "main"
|
|
9
|
+
description: "Generate images and illustrations with OpenAI and Gemini models."
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tlnw-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A collection of tools for various tasks and functionalities in the Tellers Network ecosystem.
|
|
5
|
+
Author-email: Tellers Network <admin@tlnw.uk>
|
|
6
|
+
Project-URL: Homepage, https://github.com/tellersnetwork/tlnw-tools
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: PyYAML>=6.0
|
|
12
|
+
|
|
13
|
+
# Tellers Network Tools CLI (`tlnw-tools`)
|
|
14
|
+
|
|
15
|
+
`tlnw-tools` is a modular, dynamic command-line interface (CLI) to manage and run a collection of independent tools in the Tellers Network ecosystem. Each tool is developed as a separate package published to PyPI and registered in `tools.yml`. When invoked, the main CLI dynamically verifies, downloads (if missing), and executes the tool either in-process (by default) or as a subprocess.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
- **Dynamic Tool Execution**: Run tools registered in `tools.yml` directly using `tlnw-tools <tool>`.
|
|
19
|
+
- **Automatic Packaging Management**: Automatically downloads, installs, or updates tools dynamically via `pip` on invocation.
|
|
20
|
+
- **Configurable Isolation**: Supports executing tools either via in-process module importing or subprocess delegation, configurable on a per-tool basis in `tools.yml`.
|
|
21
|
+
- **Consistent CLI**: Standard common arguments like `--help` and `--debug`.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
You can install the main CLI via PyPI:
|
|
25
|
+
```bash
|
|
26
|
+
pip install tlnw-tools
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration (`tools.yml`)
|
|
30
|
+
The main CLI registry lies in `tools.yml` located in the root of the project (or can be customized):
|
|
31
|
+
```yaml
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
description: "A collection of tools for various tasks and functionalities in the Tellers Network ecosystem."
|
|
34
|
+
tools:
|
|
35
|
+
- name: "generate-image"
|
|
36
|
+
package: "tlnw-generate-image"
|
|
37
|
+
execution_mode: "inprocess_import"
|
|
38
|
+
module: "tlnw_generate_image.cli"
|
|
39
|
+
entry_point: "main"
|
|
40
|
+
description: "Generate images and illustrations with OpenAI and Gemini models."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
```bash
|
|
45
|
+
# General help
|
|
46
|
+
tlnw-tools --help
|
|
47
|
+
|
|
48
|
+
# Running a registered tool (e.g., generate-image)
|
|
49
|
+
tlnw-tools generate-image --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Tool: `generate-image`
|
|
55
|
+
|
|
56
|
+
`generate-image` is the first unified tool package. It integrates and harmonizes image generation capabilities for story folders and specific scenes across both OpenAI (DALL-E 2/3, `gpt-image-1.5`) and Google Imagen 4.
|
|
57
|
+
|
|
58
|
+
### Usage
|
|
59
|
+
```bash
|
|
60
|
+
tlnw-tools generate-image <story_folders> [options]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Options
|
|
64
|
+
- `--model <model>`: Specify model to use. Defaults to `gpt-image-1.5`. Available models: `dall-e-3`, `gpt-image-1.5`, `imagen-4`, etc.
|
|
65
|
+
- `--quality <quality>`: Normalize quality across OpenAI and Gemini. Choices: `low`, `medium`, `high`, `standard`, `hd`, `auto`.
|
|
66
|
+
- `--landscape` / `--portrait`: Control dimensions. Defaults to square (1024x1024).
|
|
67
|
+
- `--size`: Manually specify dimensions (e.g., `1024x1024`).
|
|
68
|
+
- `--reference`: Supply reference images.
|
|
69
|
+
- `--output-prefix`: Prepend a custom prefix to generated files.
|
|
70
|
+
- `--skip-update`: Avoid updating the Jekyll/Hugo `index.md` frontmatter.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_loader.py
|
|
4
|
+
tlnw_tools/__init__.py
|
|
5
|
+
tlnw_tools/cli.py
|
|
6
|
+
tlnw_tools/tools.yml
|
|
7
|
+
tlnw_tools.egg-info/PKG-INFO
|
|
8
|
+
tlnw_tools.egg-info/SOURCES.txt
|
|
9
|
+
tlnw_tools.egg-info/dependency_links.txt
|
|
10
|
+
tlnw_tools.egg-info/entry_points.txt
|
|
11
|
+
tlnw_tools.egg-info/requires.txt
|
|
12
|
+
tlnw_tools.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyYAML>=6.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tlnw_tools
|