Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
967 changes: 967 additions & 0 deletions PREFERENCES.md

Large diffs are not rendered by default.

43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Available Commands:
domain Access domain commands
image Access image commands
load-balancer Access load-balancer commands
preferences Manage user preferences
region Access region commands
server Access server commands
size Access size commands
Expand Down Expand Up @@ -79,18 +80,19 @@ $ bl

usage: bl [OPTIONS] COMMAND

bl is a command-line interface for the binaryLane API
bl is a command-line interface for the BinaryLane API

Options:
--help Display available commands and descriptions

Available Commands:
account Access account commands
action Access action commands
configure Configure access to binaryLane API
configure Configure access to BinaryLane API
domain Access domain commands
image Access image commands
load-balancer Access load-balancer commands
preferences Manage user preferences
region Access region commands
server Access server commands
size Access size commands
Expand Down Expand Up @@ -160,7 +162,7 @@ Server creation is provided by the `bl server create` command. Use `--help` to
view all arguments and parameters:

```
$ bl server list --help
$ bl server create --help
usage: bl server create [OPTIONS] --size SIZE --image IMAGE --region REGION [PARAMETERS]

Create a new server.
Expand Down Expand Up @@ -353,6 +355,41 @@ soon as the BinaryLane API accepts the requested command. To do so, include the
$ bl server create --size std-min --image ubuntu-22.04-lts --region syd --async
```

## Preferences

Store commonly used options to streamline your workflow:

```bash
# Set default region, size, and image for server creation
bl preferences set default-region syd
bl preferences set default-size std-min
bl preferences set default-image ubuntu-24.04

# Now create servers more quickly
bl server create --name myserver
# Region, size, and image will use your preferences
```

**Terminal Settings**

- `terminal-width` - Set terminal width for help text (numeric value, or null for auto-detection)

Example:
```bash
bl preferences set terminal-width 120
```

### Priority

Preferences provide defaults that can always be overridden:

1. Command-line arguments (highest priority)
2. Environment variables
3. Preferences (`bl preferences set`)
4. Built-in defaults (lowest priority)

See [PREFERENCES.md](PREFERENCES.md) for complete documentation.

### Configuration file

`bl configure` creates a configuration file containing the API token, and reads
Expand Down
43 changes: 43 additions & 0 deletions src/binarylane/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,49 @@ def save(self) -> None:
str(self.api_development).lower() if self.api_development != default.api_development else None
)

# Output preferences - only save if explicitly set
if self.output_format:
config_options[OptionName.OUTPUT_FORMAT] = self.output_format
if self.show_header is not None:
config_options[OptionName.SHOW_HEADER] = str(self.show_header).lower()

# Per-command format preferences
for opt in [
OptionName.FORMAT_IMAGES,
OptionName.FORMAT_SERVERS,
OptionName.FORMAT_DOMAINS,
OptionName.FORMAT_VPCS,
OptionName.FORMAT_LOAD_BALANCERS,
OptionName.FORMAT_SSH_KEYS,
]:
value = self.get_option(opt)
if value:
config_options[opt] = value

# Server creation defaults
for opt in [
OptionName.DEFAULT_REGION,
OptionName.DEFAULT_SIZE,
OptionName.DEFAULT_IMAGE,
OptionName.DEFAULT_SSH_KEYS,
OptionName.DEFAULT_USER_DATA,
OptionName.DEFAULT_PASSWORD,
OptionName.DEFAULT_VPC,
]:
value = self.get_option(opt)
if value:
config_options[opt] = value

# Boolean defaults
if self.default_backups is not None:
config_options[OptionName.DEFAULT_BACKUPS] = str(self.default_backups).lower()
if self.default_port_blocking is not None:
config_options[OptionName.DEFAULT_PORT_BLOCKING] = str(self.default_port_blocking).lower()

# Terminal settings
if self.terminal_width:
config_options[OptionName.TERMINAL_WIDTH] = str(self.terminal_width)

# Write configuration to disk
file = self.get_source(src.FileSource)
file.save(config_options)
141 changes: 141 additions & 0 deletions src/binarylane/config/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,43 @@


class OptionName(str, Enum):
# Existing API options
API_URL = "api-url"
API_TOKEN = "api-token"
API_DEVELOPMENT = "api-development"
CONFIG_SECTION = "context"

# Output preferences
OUTPUT_FORMAT = "output-format"
SHOW_HEADER = "show-header"

# Per-command format preferences
FORMAT_IMAGES = "format-images"
FORMAT_SERVERS = "format-servers"
FORMAT_DOMAINS = "format-domains"
FORMAT_VPCS = "format-vpcs"
FORMAT_LOAD_BALANCERS = "format-load-balancers"
FORMAT_SSH_KEYS = "format-ssh-keys"
FORMAT_ACTIONS = "format-actions"
FORMAT_SIZES = "format-sizes"
FORMAT_REGIONS = "format-regions"
FORMAT_INVOICES = "format-invoices"
FORMAT_SOFTWARE = "format-software"

# Server creation defaults
DEFAULT_REGION = "default-region"
DEFAULT_SIZE = "default-size"
DEFAULT_IMAGE = "default-image"
DEFAULT_BACKUPS = "default-backups"
DEFAULT_SSH_KEYS = "default-ssh-keys"
DEFAULT_USER_DATA = "default-user-data"
DEFAULT_PORT_BLOCKING = "default-port-blocking"
DEFAULT_PASSWORD = "default-password"
DEFAULT_VPC = "default-vpc"

# Terminal settings
TERMINAL_WIDTH = "terminal-width"

def __str__(self) -> str:
return self.value

Expand Down Expand Up @@ -89,3 +121,112 @@ def api_development(self) -> bool:
@property
def config_section(self) -> str:
return self.required_option(OptionName.CONFIG_SECTION)

# Output preference properties

@property
def output_format(self) -> Optional[str]:
return self.get_option(OptionName.OUTPUT_FORMAT)

@property
def show_header(self) -> Optional[bool]:
value = self.get_option(OptionName.SHOW_HEADER)
return self.to_bool(value) if value else None

# Per-command format preference properties

@property
def format_images(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_IMAGES)

@property
def format_servers(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_SERVERS)

@property
def format_domains(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_DOMAINS)

@property
def format_vpcs(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_VPCS)

@property
def format_load_balancers(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_LOAD_BALANCERS)

@property
def format_ssh_keys(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_SSH_KEYS)

@property
def format_actions(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_ACTIONS)

@property
def format_sizes(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_SIZES)

@property
def format_regions(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_REGIONS)

@property
def format_invoices(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_INVOICES)

@property
def format_software(self) -> Optional[str]:
return self.get_option(OptionName.FORMAT_SOFTWARE)

# Server creation default properties

@property
def default_region(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_REGION)

@property
def default_size(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_SIZE)

@property
def default_image(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_IMAGE)

@property
def default_backups(self) -> Optional[bool]:
value = self.get_option(OptionName.DEFAULT_BACKUPS)
return self.to_bool(value) if value else None

@property
def default_ssh_keys(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_SSH_KEYS)

@property
def default_user_data(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_USER_DATA)

@property
def default_port_blocking(self) -> Optional[bool]:
value = self.get_option(OptionName.DEFAULT_PORT_BLOCKING)
return self.to_bool(value) if value else None

@property
def default_password(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_PASSWORD)

@property
def default_vpc(self) -> Optional[str]:
return self.get_option(OptionName.DEFAULT_VPC)

# Terminal setting properties

@property
def terminal_width(self) -> Optional[int]:
value = self.get_option(OptionName.TERMINAL_WIDTH)
if value and value.lower() != "auto":
try:
return int(value)
except ValueError:
return None
return None
8 changes: 7 additions & 1 deletion src/binarylane/console/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from binarylane.console.runners import Descriptor

__all__ = ["descriptors"]
descriptors: List[Descriptor] = list(api.descriptors) + [

# Filter out the auto-generated server create, we'll replace it with our wrapper
descriptors: List[Descriptor] = [d for d in api.descriptors if d.name != "server create"] + [
Descriptor(".commands.configure", "configure", "Configure access to BinaryLane API"),
Descriptor(".commands.preferences_get", "preferences get", "Display a preference value"),
Descriptor(".commands.preferences_set", "preferences set", "Set or unset a preference value"),
Descriptor(".commands.preferences_show", "preferences show", "Display preferences for a command or resource"),
Descriptor(".commands.server_create", "server create", "Create a new server"),
Descriptor(".commands.version", "version", "Show the current version"),
]
90 changes: 90 additions & 0 deletions src/binarylane/console/commands/preferences_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

from typing import TYPE_CHECKING, List

from binarylane.config.options import OptionName

from binarylane.console.runners import ExitCode, Runner

if TYPE_CHECKING:
from binarylane.console.parser import Parser


class Command(Runner):
"""Display a preference value for a given key, or list all set preferences"""

def configure(self, parser: Parser) -> None:
parser.add_argument(
"key",
nargs="?", # Make key optional
help="Preference key to retrieve (omit to list all set preferences)",
)

def run(self, args: List[str]) -> None:
if args == [self.CHECK]:
return

# Check for help first
if args and args[0] in [self.HELP, "-h", "--help"]:
self.parse(args)
return

# If no arguments, list all set preferences
if not args:
self._list_all_preferences()
return

# Get specific preference
key = args[0]

# Validate key is a known option
try:
option = OptionName(key)
except ValueError:
print(f"Unknown preference key: {key}")
print("\nValid keys:")
for opt in OptionName:
print(f" {opt.value}")
self.error(ExitCode.API, "Invalid preference key")

value = self._context.get_option(option)
if value is None:
print(f"{key} is not set")
else:
print(f"{key} = {value}")

def _list_all_preferences(self) -> None:
"""List all preferences that have been set"""
# Sensitive keys to exclude from listing
sensitive_keys = {"api-token", "default-password"}

set_preferences = []

for option in OptionName:
# Skip sensitive values
if option.value in sensitive_keys:
continue

value = self._context.get_option(option)
if value is not None:
set_preferences.append((option.value, value))

if not set_preferences:
print("No preferences are currently set.")
print("\nTo set a preference, use:")
print(" bl preferences set KEY VALUE")
print("\nAvailable preference keys:")
for opt in OptionName:
print(f" {opt.value}")
else:
print("Currently set preferences:")
print()
# Find the longest key name for formatting
max_key_len = max(len(key) for key, _ in set_preferences)
for key, value in sorted(set_preferences):
print(f" {key:<{max_key_len}} = {value}")
print()
print(f"Total: {len(set_preferences)} preference(s) set")
print()
print("Note: Sensitive values (api-token, default-password) are not listed.")
print(" Use 'bl preferences get <key>' to retrieve them individually.")
Loading
Loading