Skip to content
Open
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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def stamp_api_version():

setup(
name="science-synapse",
version="2.7.7",
version="2.7.8",
description="Client library and CLI for the Synapse API",
author="Science Team",
author_email="team@science.xyz",
Expand Down
41 changes: 21 additions & 20 deletions synapse/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
taps,
settings,
)
from synapse.cli.errors import run_action
from synapse.utils.discover import find_device_by_name


Expand All @@ -47,7 +48,10 @@ def setup_device_uri(args):


def main():
logging.basicConfig(level=logging.INFO, handlers=[RichHandler()])
logging.basicConfig(
level=logging.INFO,
handlers=[RichHandler(show_path=False)],
)
parser = argparse.ArgumentParser(
description="Synapse Device Manager",
formatter_class=lambda prog: argparse.HelpFormatter(prog, width=124),
Expand Down Expand Up @@ -85,27 +89,24 @@ def main():
deploy_model.add_commands(subparsers)
args = peripherals.parse_args_with_passthrough(parser)

# If we need to setup the device URI, do that now
args = setup_device_uri(args)
if not args:
return
def run_parsed_command():
# If we need to setup the device URI, do that now
resolved_args = setup_device_uri(args)
if not resolved_args:
return False

if hasattr(resolved_args, "func"):
return resolved_args.func(resolved_args)

try:
if hasattr(args, "func"):
args.func(args)
else:
parser.print_help()
except Exception as e:
console = Console()
console.log(f"[bold red] Uncaught error during function. Why: {e}")
parser.print_help()
except KeyboardInterrupt:
print("User cancelled request")
return None

return run_action(
run_parsed_command,
console=Console(stderr=True),
verbose=args.verbose,
)


if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"Uncaught error in CLI. Why: {e}")
sys.exit(1)
sys.exit(main())
12 changes: 9 additions & 3 deletions synapse/cli/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rich.console import Console, Group
from rich.live import Live
from rich.panel import Panel
from synapse.cli.errors import error_message
from rich.spinner import Spinner
from rich.text import Text

Expand Down Expand Up @@ -81,7 +82,7 @@ def deploy_package(ip_address, deb_package_path):
package_filename = os.path.basename(deb_package_path)
console.clear_live()

device = syn.Device(ip_address, False)
device = syn.Device(ip_address, False, raise_rpc_errors=True)
metadata = create_metadata(deb_package_path, console)
console.print(
f"[bold green]Deploying:[/bold green] [cyan]{package_filename}[/cyan]"
Expand Down Expand Up @@ -178,7 +179,9 @@ def chunk_generator():
break

# Add the error message at the bottom
display_items.append(f"[bold red]Error: {str(e)}[/bold red]")
display_items.append(
f"[bold red]Error:[/bold red] {error_message(e, device.verbose)}"
)

# Update the panel with progress and error
response_panel.renderable = Group(*display_items)
Expand All @@ -196,7 +199,10 @@ def chunk_generator():
display_items.append(f"[green]✓[/green] Step {i + 1}: {resp}")

# Add the error message
display_items.append(f"[bold red]Error during setup: {str(e)}[/bold red]")
display_items.append(
"[bold red]Error:[/bold red] "
f"Deployment setup failed: {error_message(e, device.verbose)}"
)

# Update the panel with progress and error
response_panel.renderable = Group(*display_items)
Expand Down
58 changes: 58 additions & 0 deletions synapse/cli/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

from collections.abc import Callable
from typing import Optional

import grpc
from rich.console import Console
from rich.text import Text


def error_message(error: BaseException, verbose: bool = False) -> str:
"""Return a concise user-facing message for an exception."""
if not isinstance(error, grpc.RpcError):
return str(error) or error.__class__.__name__

details = error.details()
message = details or "The device did not provide an error message."
if not verbose:
return message

code = error.code()
code_name = getattr(code, "name", str(code)) if code is not None else "UNKNOWN"
return f"gRPC {code_name}: {message}"


def print_error(
console: Console,
error: BaseException,
*,
context: Optional[str] = None,
verbose: bool = False,
) -> None:
message = error_message(error, verbose=verbose)
prefix = f"{context}: " if context else ""
console.print("[bold red]Error:[/bold red]", Text(f"{prefix}{message}"))


def run_action(
action: Callable[[], object],
*,
console: Console,
verbose: bool = False,
) -> int:
"""Run a parsed CLI action and translate runtime failures to exit codes."""
try:
result = action()
return 1 if result is False else 0
except KeyboardInterrupt:
console.print("[yellow]Operation cancelled.[/yellow]")
return 130
except grpc.RpcError as error:
print_error(console, error, verbose=verbose)
return 1
except Exception as error:
print_error(console, error)
if verbose:
console.print_exception(show_locals=False)
return 1
9 changes: 5 additions & 4 deletions synapse/cli/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from rich.prompt import Confirm

from synapse import Device
from synapse.cli.errors import print_error
import synapse.client.sftp as sftp
from synapse.utils.file import format_mode, format_time, filesize_binary

Expand Down Expand Up @@ -105,7 +106,7 @@ def ls(args):
file_attr = sftp_conn.listdir_attr(args.path)
print_file_list(file_attr, console)
except Exception as e:
console.print(f"[bold red]Failed to list directory:[/bold red] {e}")
print_error(console, e, context="Failed to list directory")

sftp.close_sftp(ssh, sftp_conn)

Expand Down Expand Up @@ -155,7 +156,7 @@ def setup_connection(
forget_password: bool,
console: Console,
) -> Optional[tuple[paramiko.SSHClient, paramiko.SFTPClient]]:
dev_name = Device(uri).get_name()
dev_name = Device(uri, raise_rpc_errors=True).get_name()
password = find_password(
dev_name, env_file
) # Check if password is provided or stored in env file
Expand Down Expand Up @@ -311,7 +312,7 @@ def update_progress(transferred: int, total: int):

sftp_conn.get(remote_path, local_path, callback=update_progress)
except paramiko.SFTPError as e:
console.print(f"[bold red]Failed to download file:[/bold red] {e}")
print_error(console, e, context="Failed to download file")
return


Expand Down Expand Up @@ -343,7 +344,7 @@ def remove_file(
):
sftp_conn.remove(remote_path)
except Exception as e:
console.print(f"[bold red]Failed to remove file:[/bold red] {e}")
print_error(console, e, context="Failed to remove file")
return

console.print(f"[bold green]File removed:[/bold green] [blue]{remote_path}")
Expand Down
45 changes: 40 additions & 5 deletions synapse/cli/offline_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,26 +160,39 @@ def compute_fft(data, sample_rate):

def plot(args):
logger = setup_logging()
console = Console()

# NOTE(gilbert): we want to support the previous plotting code but we are moving to hdf5 saving and plotting
# Short circuit for now and just use the hdf5 plotting code
if args.data is not None:
if not os.path.isfile(args.data):
console.print(
f"[bold red]Error:[/bold red] Data file not found: {args.data}"
)
return False
_, file_extension = os.path.splitext(args.data)
if file_extension == ".h5":
return plot_h5(args)
if file_extension not in (".bin", ".dat", ".jsonl"):
console.print(
"[bold red]Error:[/bold red] Unsupported data file format. "
"Expected .h5, .bin, .dat, or .jsonl."
)
return False

if args.dir and not os.path.isdir(args.dir):
console.print(
f"[bold red]Error:[/bold red] Recording directory not found: {args.dir}"
)
return False

console = Console()
console.print(
"[yellow bold]Legacy plotting is deprecated, please use the hdf5 files going forward[/yellow bold]"
)
console.print(
"[yellow bold]Use --data <path_to_hdf5_file> to plot hdf5 files[/yellow bold]"
)

app = QtWidgets.QApplication.instance()
if not app:
app = QtWidgets.QApplication(sys.argv)

data_file = None
config_file = None
if args.dir:
Expand All @@ -199,6 +212,28 @@ def plot(args):
if args.config:
config_file = args.config

if data_file is None:
console.print(
"[bold red]Error:[/bold red] No recording data found. "
"Specify --data or --dir."
)
return False
if config_file is None:
console.print(
"[bold red]Error:[/bold red] Legacy recordings require a "
"configuration file. Specify --config or use --dir."
)
return False
if not os.path.isfile(config_file):
console.print(
f"[bold red]Error:[/bold red] Configuration file not found: {config_file}"
)
return False

app = QtWidgets.QApplication.instance()
if not app:
app = QtWidgets.QApplication(sys.argv)

# Start with loading the config
sampling_freq, num_channels, channel_ids = load_config(config_file)
if args.channels:
Expand Down
43 changes: 32 additions & 11 deletions synapse/cli/query.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import asyncio
import grpc
from threading import Thread
import time
import sys
Expand All @@ -20,14 +21,16 @@
from rich.live import Live
from rich.panel import Panel

from synapse.cli.errors import print_error


class StreamingQueryClient:
def __init__(self, uri, verbose=False):
self.uri = uri
self.verbose = verbose
self.console = Console()

self.device = syn.Device(self.uri, self.verbose)
self.device = syn.Device(self.uri, self.verbose, raise_rpc_errors=True)
if self.verbose:
info = self.device.info()
self.console.log(info)
Expand All @@ -45,12 +48,20 @@ def close(self):

def tail_logs_background(self):
self.last_log_line = ""
for log in self.device.tail_logs():
if self.last_log_line != log.message:
self.last_log_line = log.message
self.new_log_event.set()
if self.log_stop_event.is_set():
break
try:
for log in self.device.tail_logs():
if self.last_log_line != log.message:
self.last_log_line = log.message
self.new_log_event.set()
if self.log_stop_event.is_set():
break
except grpc.RpcError as error:
print_error(
self.console,
error,
context="Log stream failed",
verbose=self.verbose,
)

def stream_query(self, request):
query_type = request.request.query_type
Expand All @@ -63,7 +74,12 @@ def stream_query(self, request):
self.console.log(f"[bold red]Unknown stream request: {query_type}")
return False
except Exception as e:
self.console.log(f"[bold red] Uncaught exception during stream: {e}")
print_error(
self.console,
e,
context="Streaming query failed",
verbose=self.verbose,
)
return False
except KeyboardInterrupt:
self.console.log("[yellow] Operation cancelled by user")
Expand Down Expand Up @@ -103,7 +119,7 @@ def update_status():

if response.code != 0 or not response.self_test:
self.console.log(
f"[bold red] Failed self test, why: {response.message}"
f"[bold red]Self test failed:[/bold red] {response.message}"
)
return False

Expand Down Expand Up @@ -210,7 +226,7 @@ def update_progress():

failed_ids = [m.electrode_id for m in failed_batch]
progress.console.log(
f"Failed to measure impedance for {failed_ids}, why: {response.message}"
f"Failed to measure impedance for {failed_ids}: {response.message}"
)
for sample in failed_batch:
progress.console.log(
Expand Down Expand Up @@ -317,5 +333,10 @@ def load_config_from_file(path_to_config):
print("Failed to stream query for device")
sys.exit(1)
except Exception as e:
print(f"Failed to stream query. Why: {e}")
print_error(
Console(stderr=True),
e,
context="Streaming query failed",
verbose=args.verbose,
)
sys.exit(1)
Loading
Loading