Tome Scripts In-Depth
Now that you've seen the basics of creating and running a simple command in the Quickstart, this guide dives deeper into building more structured and powerful tome commands with Python.
We'll explore how to:
- Organize related actions using subcommands.
- Create flexible output presentations with formatters.
- Persist and retrieve data using tome's Store API.
1. The utils:todo
command and its subcommands
For this guide, we'll build a simple command-line To-Do list manager called
utils:todo
the will have several subcommands to allow us to:
tome utils:todo add
: Add new tasks.tome utils:todo list
: List existing tasks.tome utils:todo mark
: Mark tasks as done.tome utils:todo remove
: Remove tasks.
2. Setting Up the Script
First, if you haven't already, create a directory for your project (e.g.,
my-scripts
) and navigate into it. Then, use tome new
to create the initial
script for the todo
command within a utils
namespace. The main function can
be named todo
.
Also install it as editable at this point so we can check our changes as we introduce them.
3. Implementing Subcommands
Let's edit utils/todo.py
. Let's start by defining the main todo
command and
its first subcommand, add
. For data persistence, we'll use a simple JSON file
stored via tome's Store API.
from tome.command import tome_command
from tome.api.output import TomeOutput
@tome_command()
def todo(tome_api, parser, *args):
"""
A simple command-line To-Do list manager.
"""
pass
@tome_command(parent=todo)
def add(tome_api, parser, *args):
"""Adds a new task to the list."""
parser.add_argument('description', help="The description of the task.")
parsed_args = parser.parse_args(*args)
TomeOutput(stdout=True).info(f"Task '{parsed_args.description}' added (not saved yet).")
Explanation of Subcommand Structure:
- The main
todo
function is decorated with@tome_command()
but just declares apass
because it takes no arguments it's just the entry point for the subcommands. - The
add
subcommand is decorated with@tome_command(parent=todo)
, making it subcommands oftodo
. - The
add
subcommand defines its own arguments using theparser
object it receives.
At this point this does not do much:
Let's make some improvements:
4. Using the Store API (tome_api.store.folder
) for Persistent Data
Our add
subcommand, as defined in the previous step, prints a message but
doesn't actually save the task anywhere. To make our To-Do list useful, we need
to store the tasks persistently so they can be retrieved later.
This is where tome's Store API comes in handy. The tome_api
object passed
to your command functions has an attribute store.folder
, which provides a
consistent, managed directory path within your tome home (usually
~/.tome/storage/
). Your scripts can use this location to save and read data
files.
Let's modify our utils/todo.py
script. We will update the add
subcommand to
save the new task into a JSON file named mytasks.json
, located within a
utils_todo
subdirectory inside the path provided by tome_api.store.folder
.
from tome.command import tome_command
from tome.api.output import TomeOutput
import json
import os
TASKS_FILE_NAME = "mytasks.json"
@tome_command()
def todo(tome_api, parser, *args):
"""
A simple command-line To-Do list manager.
"""
pass
@tome_command(parent=todo)
def add(tome_api, parser, *args):
"""Adds a new task to the list."""
parser.add_argument('description', help="The description of the task.")
parsed_args = parser.parse_args(*args)
task_description = parsed_args.description
utility_storage_path = os.path.join(tome_api.store.folder, "utils_todo")
os.makedirs(utility_storage_path, exist_ok=True)
tasks_file_path = os.path.join(utility_storage_path, TASKS_FILE_NAME)
tasks = []
if os.path.exists(tasks_file_path):
try:
with open(tasks_file_path, 'r') as f:
tasks = json.load(f)
except (json.JSONDecodeError, IOError):
tasks = []
new_task = {"description": task_description}
tasks.append(new_task)
try:
with open(tasks_file_path, 'w') as f:
json.dump(tasks, f, indent=2)
TomeOutput(stdout=True).info(f"Task '{task_description}' saved in '{tasks_file_path}'.")
except IOError:
TomeOutput().error(f"Could not save task to '{tasks_file_path}'.")
Explanation of the changes:
- A constant
TASKS_FILE_NAME
is defined. - Inside
add
,utility_storage_path
is created usingtome_api.store.folder
combined with a subdirectoryutils_todo
for better organization within the global store.os.makedirs(utility_storage_path, exist_ok=True)
ensures this path exists. tasks_file_path
is the full path to ourmytasks.json
.- We attempt to load existing tasks from this file. If the file doesn't exist or
is invalid, we start with an empty list (
tasks = []
). - The
new_task
dictionary currently only stores thedescription
. - The updated
tasks
list is then saved back tomytasks.json
usingjson.dump()
. - A confirmation message, including the path to the saved file, is printed.
Now, when you run the add
subcommand, the task is saved:
$ tome utils:todo add "Buy groceries"
Task 'Buy groceries' saved in '~/.tome/storage/utils_todo/mytasks.json'.
$ cat '~/.tome/storage/utils_todo/mytasks.json'
You can inspect the mytasks.json
file (the exact path will be shown in your
output) to see the saved tasks:
This demonstrates how the Store API provides a clean way to handle persistent data for your commands without needing to manage complex pathing or worry about where to place user-specific data.
5. Standarazing Output using Formatters
Our subcommands currently is printing a success message to stdout
but tome
provides a way of presenting information in a more structured? way using
formatters
. Let's define formatters for our list
subcommand. We'll add a
text
formatter and a JSON
formatter.
Modify utils/todo.py
to include these formatter functions and update the
@tome_command
decorator for list
:
from tome.command import tome_command
from tome.api.output import TomeOutput
from tome.errors import TomeException
import json
import os
TASKS_FILE_NAME = "mytasks.json"
def todo_text_formatter(data_dict):
if data_dict.get("status") == "success" and "message" in data_dict:
TomeOutput(stdout=True).info(data_dict["message"])
elif data_dict.get("status") == "error" and "error" in data_dict:
raise TomeException(data_dict["error"])
def todo_json_formatter(data_dict):
TomeOutput(stdout=True).print_json(json.dumps(data_dict, indent=2))
if data_dict.get("status") == "error" and "error" in data_dict:
raise TomeException(data_dict["error"])
@tome_command()
def todo(tome_api, parser, *args):
"""
A simple command-line To-Do list manager.
"""
pass
@tome_command(parent=todo, formatters={"text": todo_text_formatter, "json": todo_json_formatter})
def add(tome_api, parser, *args):
"""Adds a new task to the list."""
parser.add_argument('description', help="The description of the task.")
parsed_args = parser.parse_args(*args)
task_description = parsed_args.description
utility_storage_path = os.path.join(tome_api.store.folder, "utils_todo")
os.makedirs(utility_storage_path, exist_ok=True)
tasks_file_path = os.path.join(utility_storage_path, TASKS_FILE_NAME)
tasks = []
if os.path.exists(tasks_file_path):
try:
with open(tasks_file_path, 'r') as f:
tasks = json.load(f)
except (json.JSONDecodeError, IOError):
tasks = []
new_task = {"description": task_description}
tasks.append(new_task)
try:
with open(tasks_file_path, 'w') as f:
json.dump(tasks, f, indent=2)
return {
"action": "add",
"status": "success",
"message": f"Task '{task_description}' added.",
"description": task_description
}
except IOError as e:
return {
"action": "add",
"status": "error",
"error": f"Could not save task to '{tasks_file_path}': {str(e)}",
"description": task_description
}
Understanding the Changes:
- The
@tome_command()
decorator foradd
was updated with aformatters
argument. - The
add
function nowreturn
s a dictionary, whether for success or a validation error. - We defined
todo_text_formatter
andtodo_json_formatter
to handle the dictionary returned byadd
you can select which one you want to use when runningutils:todo add
with the--format
argument. Thetext
one is used by default if the--format
argument is not passed. - The
todo_text_formatter
raises aTomeException
if it finds an error in the data, ensuring tome reports it. - The
todo_json_formatter
prints the JSON (which will include the error structure if present) and then raisesTomeException
if an error key exists, so automated tools get structured error data but the script still exits with an error code.
Now, tome automatically adds a --format
option to your command.
Try it out:
Default text output:
$ tome utils:todo add "Buy groceries"
Task 'Buy groceries' added.
$ tome utils:todo add "Take the dog out" --format=json
{
"action": "add",
"status": "success",
"message": "Task 'Take the dog out' added.",
"description": "Take the dog out"
}
Using formatters like this keeps your command's core logic separate from its presentation, making your code cleaner. Plus, you can easily offer different output styles (e.g., text for users, JSON for tools) from a single command, making it more maintainable. You can learn more about how to define and use them in the Output Formatters Reference.
6. Finishing the command
You can find the full implementation in the examples folder in the GitHub repository. Please, copy and paste from here if you want to test the complete command.
Conclusion
In this guide, you've seen how to:
- Structure commands using subcommands for better organization.
- Utilize the
tome_api.store.folder
for persistent data storage. - Make your commands return data and use formatters for flexible text and JSON output.
- Tested your command with different arguments and output formats using the
automatically provided
--format
option.
These features allow you to build more sophisticated and user-friendly command-line tools with tome. Explore the Python Scripting API Reference for more details on these and other capabilities.