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
4 changes: 3 additions & 1 deletion examples/multimodal/multimodal/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from .config import FISHJAM_ID, FISHJAM_TOKEN
from .worker import BackgroundWorker

fishjam = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN)
fishjam = FishjamClient.create_and_verify(
fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN
)


class RoomService:
Expand Down
4 changes: 3 additions & 1 deletion examples/poet_chat/poet_chat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@
with open(GREET_PATH) as prompt:
OPENAI_GREET = prompt.read()

fishjam_client = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN)
fishjam_client = FishjamClient.create_and_verify(
fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN
)
2 changes: 1 addition & 1 deletion examples/room_manager/room_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PeerAccess:

class RoomService:
def __init__(self, args: Namespace, logger: Logger):
self.fishjam_client = FishjamClient(
self.fishjam_client = FishjamClient.create_and_verify(
fishjam_id=args.fishjam_id,
management_token=args.management_token,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class RoomService:
def __init__(self):
self.fishjam = FishjamClient(
self.fishjam = FishjamClient.create_and_verify(
fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN
)
self.room = self.fishjam.create_room(
Expand Down
4 changes: 3 additions & 1 deletion examples/transcription/transcription/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from .agent import TranscriptionAgent
from .config import FISHJAM_ID, FISHJAM_TOKEN

fishjam = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN)
fishjam = FishjamClient.create_and_verify(
fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN
)


class RoomService:
Expand Down
3 changes: 3 additions & 0 deletions fishjam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Room,
RoomOptions,
)
from fishjam.errors import InvalidFishjamCredentialsError, MissingFishjamIdError

__version__ = version.__version__

Expand All @@ -39,6 +40,8 @@
"AgentOutputOptions",
"Room",
"Peer",
"MissingFishjamIdError",
"InvalidFishjamCredentialsError",
"events",
"errors",
"room",
Expand Down
46 changes: 46 additions & 0 deletions fishjam/api/_fishjam_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
from fishjam._openapi_client.types import UNSET
from fishjam.agent import Agent
from fishjam.api._client import Client
from fishjam.errors import (
InvalidFishjamCredentialsError,
NotFoundError,
UnauthorizedError,
)


@dataclass
Expand Down Expand Up @@ -154,12 +159,53 @@ def __init__(
):
"""Create a FishjamClient instance.

Does not contact the Fishjam backend — use :meth:`create_and_verify`
or :meth:`check_credentials` to verify credentials live.

Args:
fishjam_id: The unique identifier for the Fishjam instance.
management_token: The token used for authenticating management operations.
"""
super().__init__(fishjam_id=fishjam_id, management_token=management_token)

@classmethod
def create_and_verify(
cls, *, fishjam_id: str, management_token: str
) -> "FishjamClient":
"""Construct a FishjamClient and verify credentials against the backend.

Args:
fishjam_id: The unique identifier for the Fishjam instance.
management_token: The token used for authenticating management operations.

Returns:
FishjamClient: A client whose credentials have been verified.

Raises:
InvalidFishjamCredentialsError: If the ``fishjam_id`` /
``management_token`` pair is rejected by the backend.
"""
client = cls(fishjam_id=fishjam_id, management_token=management_token)
client.check_credentials()
return client

def check_credentials(self) -> None:
"""Verify configured credentials by pinging the backend.

Performs a single lightweight ``get_all_rooms`` call. A 401 or 404
from the backend is translated into
:class:`InvalidFishjamCredentialsError`; other HTTP errors propagate
as their normal mapped types.

Raises:
InvalidFishjamCredentialsError: If the ``fishjam_id`` /
``management_token`` pair is rejected by the backend.
"""
try:
self.get_all_rooms()
except (UnauthorizedError, NotFoundError) as exc:
raise InvalidFishjamCredentialsError(*exc.args) from exc

def create_peer(
self,
room_id: str,
Expand Down
11 changes: 11 additions & 0 deletions fishjam/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from fishjam._openapi_client.types import Response


class MissingFishjamIdError(ValueError):
def __init__(self) -> None:
super().__init__("Fishjam ID is required")


class HTTPError(Exception):
""""""

Expand Down Expand Up @@ -69,3 +74,9 @@ class ConflictError(HTTPError):
def __init__(self, errors):
"""@private"""
super().__init__(errors)


class InvalidFishjamCredentialsError(HTTPError):
def __init__(self, errors):
"""@private"""
super().__init__(errors)
5 changes: 5 additions & 0 deletions fishjam/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from urllib.parse import urlparse

from fishjam.errors import MissingFishjamIdError


def validate_url(url: str) -> bool:
try:
Expand All @@ -10,6 +12,9 @@ def validate_url(url: str) -> bool:


def get_fishjam_url(fishjam_id: str) -> str:
if not fishjam_id:
raise MissingFishjamIdError()

if not validate_url(fishjam_id):
return f"https://fishjam.io/api/v1/connect/{fishjam_id}"

Expand Down
96 changes: 96 additions & 0 deletions tests/test_config_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What happened here? I'm pretty sure we use ruff, not pylint


from unittest.mock import patch

import pytest

from fishjam import FishjamClient
from fishjam.errors import (
InvalidFishjamCredentialsError,
MissingFishjamIdError,
NotFoundError,
UnauthorizedError,
)

VALID_FISHJAM_ID = "fjm_test"
VALID_MANAGEMENT_TOKEN = "tok_test"


class TestSyncValidation:
def test_empty_fishjam_id_raises(self):
with pytest.raises(MissingFishjamIdError):
FishjamClient(fishjam_id="", management_token=VALID_MANAGEMENT_TOKEN)

def test_both_provided_does_not_raise(self):
FishjamClient(
fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN
)


class TestLiveCheck:
def test_create_and_verify_raises_invalid_credentials_on_401(self):
with patch.object(
FishjamClient,
"get_all_rooms",
side_effect=UnauthorizedError("Invalid token"),
):
with pytest.raises(InvalidFishjamCredentialsError):
FishjamClient.create_and_verify(
fishjam_id=VALID_FISHJAM_ID,
management_token=VALID_MANAGEMENT_TOKEN,
)

def test_create_and_verify_raises_invalid_credentials_on_404(self):
with patch.object(
FishjamClient,
"get_all_rooms",
side_effect=NotFoundError("Fishjam not found"),
):
with pytest.raises(InvalidFishjamCredentialsError):
FishjamClient.create_and_verify(
fishjam_id=VALID_FISHJAM_ID,
management_token=VALID_MANAGEMENT_TOKEN,
)

def test_create_and_verify_returns_client_and_pings_once(self):
with patch.object(
FishjamClient, "get_all_rooms", return_value=[]
) as mock_get_all:
client = FishjamClient.create_and_verify(
fishjam_id=VALID_FISHJAM_ID,
management_token=VALID_MANAGEMENT_TOKEN,
)

assert isinstance(client, FishjamClient)
assert mock_get_all.call_count == 1

def test_check_credentials_raises_invalid_credentials_on_401(self):
client = FishjamClient(
fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN
)
with patch.object(
FishjamClient,
"get_all_rooms",
side_effect=UnauthorizedError("Invalid token"),
):
with pytest.raises(InvalidFishjamCredentialsError):
client.check_credentials()

def test_check_credentials_raises_invalid_credentials_on_404(self):
client = FishjamClient(
fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN
)
with patch.object(
FishjamClient,
"get_all_rooms",
side_effect=NotFoundError("Fishjam not found"),
):
with pytest.raises(InvalidFishjamCredentialsError):
client.check_credentials()

def test_check_credentials_returns_none_on_success(self):
client = FishjamClient(
fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN
)
with patch.object(FishjamClient, "get_all_rooms", return_value=[]):
assert client.check_credentials() is None
Loading