Compare commits

...

6 Commits

Author SHA1 Message Date
23d3118e6a keep names consistent 2025-06-13 14:30:50 +01:00
c369f4e3d5 add 0.10.0 to CHANGELOG 2025-06-13 14:19:22 +01:00
129c3f57f2 minor bump 2025-06-13 14:14:16 +01:00
42519ba294 slow down the scene tests in an attempt to avoid rate limiting issues 2025-06-13 14:11:16 +01:00
09a44b2dea add SlobsCliProtocolError for wrapping ProtocolError
handle ProtocolError(s) and reraise as SlobsCliProtocolError. This has the following benefits:
A user friendly error message
A non-zero exit code
2025-06-13 14:10:54 +01:00
f4421b3351 call next(iter()) on excgroup.exceptions to convery intent a little better
(we dont intend to iterate through them, we just want to raise the first one)
2025-06-13 14:04:05 +01:00
11 changed files with 110 additions and 44 deletions

View File

@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [0.10.0] - 2025-06-13
### Changed
- scene commands are prone to raise ProtocolErrors if called too quickly in succession. To make these errors a little more user friendly the following changes have been made:
- They print error messages to stderr
- They return exit code 2
# [0.9.0] - 2025-06-12
### Added

View File

@ -1,3 +1,3 @@
"""module for package metadata."""
__version__ = '0.9.3'
__version__ = '0.10.0'

View File

@ -86,8 +86,8 @@ async def mute(ctx: click.Context, source_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@audio.command()
@ -117,8 +117,8 @@ async def unmute(ctx: click.Context, source_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@audio.command()
@ -151,8 +151,8 @@ async def toggle(ctx: click.Context, source_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@audio.command()
@ -182,5 +182,5 @@ async def status(ctx: click.Context, source_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable

View File

@ -1,5 +1,7 @@
"""module for custom exceptions in Slobs CLI."""
import json
import asyncclick as click
@ -14,3 +16,31 @@ class SlobsCliError(click.ClickException):
def show(self):
"""Display the error message in red."""
click.secho(f'Error: {self.message}', fg='red', err=True)
class SlobsCliProtocolError(SlobsCliError):
"""Converts pyslobs ProtocolError to a SlobsCliProtocolError."""
def __init__(self, message: str):
"""Initialize the SlobsCliProtocolError with a message."""
protocol_message_to_dict = json.loads(
str(message).replace('"', '\\"').replace("'", '"')
)
super().__init__(
protocol_message_to_dict.get('message', 'Unable to parse error message')
)
self.exit_code = 2
self.protocol_code = protocol_message_to_dict.get('code', 'Unknown error code')
def show(self):
"""Display the protocol error message in red."""
match self.protocol_code:
case -32600:
click.secho(
'Oops! Looks like we hit a rate limit for this command. Please try again later.',
fg='red',
err=True,
)
case _:
# Fall back to the base error display for unknown protocol codes
super().show()

View File

@ -38,8 +38,8 @@ async def start(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@record.command()
@ -67,8 +67,8 @@ async def stop(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@record.command()

View File

@ -37,8 +37,8 @@ async def start(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@replaybuffer.command()
@ -65,8 +65,8 @@ async def stop(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@replaybuffer.command()

View File

@ -2,11 +2,11 @@
import asyncclick as click
from anyio import create_task_group
from pyslobs import ScenesService, TransitionsService
from pyslobs import ProtocolError, ScenesService, TransitionsService
from terminaltables3 import AsciiTable
from .cli import cli
from .errors import SlobsCliError
from .errors import SlobsCliError, SlobsCliProtocolError
@cli.group()
@ -56,9 +56,14 @@ async def list(ctx: click.Context, id: bool = False):
conn.close()
async with create_task_group() as tg:
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
try:
async with create_task_group() as tg:
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* ProtocolError as excgroup:
p_error = next(iter(excgroup.exceptions))
raisable = SlobsCliProtocolError(str(p_error))
raise raisable
@scene.command()
@ -77,9 +82,14 @@ async def current(ctx: click.Context, id: bool = False):
)
conn.close()
async with create_task_group() as tg:
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
try:
async with create_task_group() as tg:
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* ProtocolError as excgroup:
p_error = next(iter(excgroup.exceptions))
raisable = SlobsCliProtocolError(str(p_error))
raise raisable
@scene.command()
@ -138,12 +148,16 @@ async def switch(
break
else: # If no scene by the given name was found
conn.close()
raise SlobsCliError(f"Scene '{scene_name}' not found.")
raise SlobsCliError(f'Scene "{scene_name}" not found.')
try:
async with create_task_group() as tg:
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
except* ProtocolError as excgroup:
p_error = next(iter(excgroup.exceptions))
raisable = SlobsCliProtocolError(str(p_error))
raise raisable

View File

@ -88,8 +88,8 @@ async def load(ctx: click.Context, scenecollection_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@scenecollection.command()
@ -136,8 +136,8 @@ async def delete(ctx: click.Context, scenecollection_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@scenecollection.command()
@ -169,5 +169,5 @@ async def rename(ctx: click.Context, scenecollection_name: str, new_name: str):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable

View File

@ -37,8 +37,8 @@ async def start(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@stream.command()
@ -65,8 +65,8 @@ async def stop(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@stream.command()

View File

@ -35,8 +35,8 @@ async def enable(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@studiomode.command()
@ -61,8 +61,8 @@ async def disable(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable
@studiomode.command()
@ -129,5 +129,5 @@ async def force_transition(ctx: click.Context):
tg.start_soon(conn.background_processing)
tg.start_soon(_run)
except* SlobsCliError as excgroup:
for e in excgroup.exceptions:
raise e
raisable = next(iter(excgroup.exceptions))
raise raisable

View File

@ -1,5 +1,6 @@
"""Test cases for scene commands in slobs_cli."""
import anyio
import pytest
from asyncclick.testing import CliRunner
@ -15,6 +16,7 @@ async def test_scene_list():
assert 'slobs-test-scene-1' in result.output
assert 'slobs-test-scene-2' in result.output
assert 'slobs-test-scene-3' in result.output
await anyio.sleep(0.2) # Avoid rate limiting issues
@pytest.mark.anyio
@ -23,7 +25,19 @@ async def test_scene_current():
runner = CliRunner()
result = await runner.invoke(cli, ['scene', 'switch', 'slobs-test-scene-2'])
assert result.exit_code == 0
await anyio.sleep(0.2) # Avoid rate limiting issues
result = await runner.invoke(cli, ['scene', 'current'])
assert result.exit_code == 0
assert 'Current active scene: slobs-test-scene-2' in result.output
await anyio.sleep(0.2) # Avoid rate limiting issues
@pytest.mark.anyio
async def test_scene_invalid_switch():
"""Test switching to an invalid scene."""
runner = CliRunner()
result = await runner.invoke(cli, ['scene', 'switch', 'invalid-scene'])
assert result.exit_code != 0
assert 'Scene "invalid-scene" not found' in result.output
await anyio.sleep(0.2) # Avoid rate limiting issues