diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77e82da..1b6e312 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,56 +3,82 @@ name: CI on: [push] jobs: - static-checks: + lint-and-format: runs-on: ubuntu-latest - strategy: &python-matrix - matrix: - python-version: - - "3.11" - - "3.12" - - "3.13" - name: static-checks steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install uv - uses: astral-sh/setup-uv@v6 + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 with: - python-version: ${{ matrix.python-version }} enable-cache: true - name: Install project dependencies - run: uv sync --locked --all-extras --dev --all-packages + run: uv sync --locked --all-extras --all-packages - - name: Format + - name: Format Check run: uv run format_check - name: Lint run: uv run lint + type-check: + runs-on: ubuntu-latest + strategy: &strategy + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + + - name: Install project dependencies + run: uv sync --locked --all-extras --all-packages + - name: Type check run: uv run pyright test: runs-on: ubuntu-latest - strategy: *python-matrix - env: - PYTHON_VERSION: ${{ matrix.python-version }} - name: test + needs: [lint-and-format] + strategy: *strategy steps: - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true - - name: Log in to GitHub Container Registry - run: echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | docker login ghcr.io -u USERNAME --password-stdin + - name: Install project dependencies + run: uv sync --locked --all-extras --all-packages - - name: Run tests - run: docker compose -f docker-compose-test.yaml up test --exit-code-from test + - name: Initialize Localtunnel + id: tunnel + run: | + npx localtunnel --port 5000 > tunnel.log 2>&1 & + + # Poll for the URL + TIMEOUT=10 + ELAPSED=0 + echo "Waiting for localtunnel to generate URL..." + + while ! grep -q "https://" tunnel.log; do + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Error: Localtunnel timed out after ${TIMEOUT}s" + cat tunnel.log + exit 1 + fi + sleep 1 + ELAPSED=$((ELAPSED + 1)) + done + + TUNNEL_URL=$(grep -o 'https://[^ ]*' tunnel.log | head -n 1) + echo "url=$TUNNEL_URL" >> $GITHUB_OUTPUT + echo "Localtunnel is live at: $TUNNEL_URL" - - name: Tear down test containers - run: docker compose -f docker-compose-test.yaml down + - name: Run tests + run: uv run pytest + env: + WEBHOOK_SERVER_URL: ${{ steps.tunnel.outputs.url }} + FISHJAM_ID: ${{ secrets.CI_FISHJAM_ID }} + FISHJAM_MANAGEMENT_TOKEN: ${{ secrets.CI_FISHJAM_MANAGEMENT_TOKEN }} diff --git a/docker-compose-test.yaml b/docker-compose-test.yaml deleted file mode 100644 index 67763ea..0000000 --- a/docker-compose-test.yaml +++ /dev/null @@ -1,75 +0,0 @@ -services: - fishjam: - image: ghcr.io/fishjam-cloud/fishjam:edge - container_name: fishjam - restart: on-failure - healthcheck: - test: > - curl --fail-with-body -H "Authorization: Bearer admin" - http://fishjam:5002/admin/health || exit 1 - interval: 3s - retries: 2 - timeout: 2s - start_period: 30s - environment: - FJ_HOST: "fishjam:5002" - FJ_ADMIN_TOKEN: "admin" - FJ_PORT: 5002 - FJ_TEST_USER_TOKEN: "development" - FJ_CHECK_ORIGIN: "false" - # Fisthank - FJ_FISHTANK_TOKEN: "foo" - FJ_FISHTANK_ADDRESS: fishtank:50051 - # Broadcaster - FJ_BROADCASTING_ENABLED: "true" - FJ_BROADCASTER_URL: "http://broadcaster:4000" - FJ_BROADCASTER_TOKEN: "broadcaster_token" - FJ_BROADCASTER_WHIP_TOKEN: "whip_token" - # Dev - LOG_LEVEL: "debug" - - ports: - - "5002:5002" - - fishtank: - image: ghcr.io/fishjam-cloud/fishtank:edge - container_name: fishtank - restart: on-failure - environment: - FT_FISHJAM_NOTIFICATIONS_ENDPOINT: http://fishjam:5002/notifications/fishtank - FT_FISHJAM_TOKEN: "foo" - FT_STRUCTURED_LOGGING: true - healthcheck: - interval: 3s - retries: 2 - timeout: 2s - start_period: 30s - ports: - - "8080:8080" - - caddy: - image: caddy - container_name: proxy - restart: unless-stopped - ports: - - 5555:5555 - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - test: - container_name: test - build: - context: . - dockerfile: tests/Dockerfile - args: - PYTHON_VERSION: ${PYTHON_VERSION:-3.11} - command: uv run pytest -s -vv - environment: - DOCKER_TEST: "TRUE" - depends_on: - fishtank: - condition: service_healthy - fishjam: - condition: service_healthy - caddy: - condition: service_started diff --git a/fishjam/_openapi_client/api/room/add_peer.py b/fishjam/_openapi_client/api/room/add_peer.py index 8d0cd25..96b33dc 100644 --- a/fishjam/_openapi_client/api/room/add_peer.py +++ b/fishjam/_openapi_client/api/room/add_peer.py @@ -5,8 +5,8 @@ from ... import errors from ...client import AuthenticatedClient, Client -from ...models.add_peer_body import AddPeerBody from ...models.error import Error +from ...models.peer_config import PeerConfig from ...models.peer_details_response import PeerDetailsResponse from ...types import Response @@ -14,7 +14,7 @@ def _get_kwargs( room_id: str, *, - body: AddPeerBody, + body: PeerConfig, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -81,13 +81,13 @@ def sync_detailed( room_id: str, *, client: AuthenticatedClient, - body: AddPeerBody, + body: PeerConfig, ) -> Response[Union[Error, PeerDetailsResponse]]: """Create peer Args: room_id (str): - body (AddPeerBody): + body (PeerConfig): Peer configuration Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -113,13 +113,13 @@ def sync( room_id: str, *, client: AuthenticatedClient, - body: AddPeerBody, + body: PeerConfig, ) -> Optional[Union[Error, PeerDetailsResponse]]: """Create peer Args: room_id (str): - body (AddPeerBody): + body (PeerConfig): Peer configuration Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -140,13 +140,13 @@ async def asyncio_detailed( room_id: str, *, client: AuthenticatedClient, - body: AddPeerBody, + body: PeerConfig, ) -> Response[Union[Error, PeerDetailsResponse]]: """Create peer Args: room_id (str): - body (AddPeerBody): + body (PeerConfig): Peer configuration Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -170,13 +170,13 @@ async def asyncio( room_id: str, *, client: AuthenticatedClient, - body: AddPeerBody, + body: PeerConfig, ) -> Optional[Union[Error, PeerDetailsResponse]]: """Create peer Args: room_id (str): - body (AddPeerBody): + body (PeerConfig): Peer configuration Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. diff --git a/fishjam/_openapi_client/models/__init__.py b/fishjam/_openapi_client/models/__init__.py index 90e0b47..2ee6c44 100644 --- a/fishjam/_openapi_client/models/__init__.py +++ b/fishjam/_openapi_client/models/__init__.py @@ -1,11 +1,11 @@ """Contains all the data models used in inputs/outputs""" -from .add_peer_body import AddPeerBody from .agent_output import AgentOutput from .audio_format import AudioFormat from .audio_sample_rate import AudioSampleRate from .error import Error from .peer import Peer +from .peer_config import PeerConfig from .peer_details_response import PeerDetailsResponse from .peer_details_response_data import PeerDetailsResponseData from .peer_metadata import PeerMetadata @@ -41,12 +41,12 @@ from .web_rtc_metadata import WebRTCMetadata __all__ = ( - "AddPeerBody", "AgentOutput", "AudioFormat", "AudioSampleRate", "Error", "Peer", + "PeerConfig", "PeerDetailsResponse", "PeerDetailsResponseData", "PeerMetadata", diff --git a/fishjam/_openapi_client/models/agent_output.py b/fishjam/_openapi_client/models/agent_output.py index c32db25..6a3b85b 100644 --- a/fishjam/_openapi_client/models/agent_output.py +++ b/fishjam/_openapi_client/models/agent_output.py @@ -6,7 +6,6 @@ ) from attrs import define as _attrs_define -from attrs import field as _attrs_field from ..models.audio_format import AudioFormat from ..models.audio_sample_rate import AudioSampleRate @@ -26,7 +25,6 @@ class AgentOutput: audio_format: Union[Unset, AudioFormat] = UNSET audio_sample_rate: Union[Unset, AudioSampleRate] = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: audio_format: Union[Unset, str] = UNSET @@ -38,7 +36,7 @@ def to_dict(self) -> dict[str, Any]: audio_sample_rate = self.audio_sample_rate.value field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({}) if audio_format is not UNSET: field_dict["audioFormat"] = audio_format @@ -69,21 +67,4 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: audio_sample_rate=audio_sample_rate, ) - agent_output.additional_properties = d return agent_output - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/fishjam/_openapi_client/models/add_peer_body.py b/fishjam/_openapi_client/models/peer_config.py similarity index 72% rename from fishjam/_openapi_client/models/add_peer_body.py rename to fishjam/_openapi_client/models/peer_config.py index 9134e63..9639d0a 100644 --- a/fishjam/_openapi_client/models/add_peer_body.py +++ b/fishjam/_openapi_client/models/peer_config.py @@ -7,7 +7,6 @@ ) from attrs import define as _attrs_define -from attrs import field as _attrs_field from ..models.peer_type import PeerType @@ -16,12 +15,13 @@ from ..models.peer_options_web_rtc import PeerOptionsWebRTC -T = TypeVar("T", bound="AddPeerBody") +T = TypeVar("T", bound="PeerConfig") @_attrs_define -class AddPeerBody: - """ +class PeerConfig: + """Peer configuration + Attributes: options (Union['PeerOptionsAgent', 'PeerOptionsWebRTC']): Peer-specific options type_ (PeerType): Peer type Example: webrtc. @@ -29,7 +29,6 @@ class AddPeerBody: options: Union["PeerOptionsAgent", "PeerOptionsWebRTC"] type_: PeerType - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.peer_options_web_rtc import PeerOptionsWebRTC @@ -43,7 +42,7 @@ def to_dict(self) -> dict[str, Any]: type_ = self.type_.value field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({ "options": options, "type": type_, @@ -81,26 +80,9 @@ def _parse_options( type_ = PeerType(d.pop("type")) - add_peer_body = cls( + peer_config = cls( options=options, type_=type_, ) - add_peer_body.additional_properties = d - return add_peer_body - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties + return peer_config diff --git a/fishjam/_openapi_client/models/peer_options_agent.py b/fishjam/_openapi_client/models/peer_options_agent.py index 713e40f..03df548 100644 --- a/fishjam/_openapi_client/models/peer_options_agent.py +++ b/fishjam/_openapi_client/models/peer_options_agent.py @@ -7,7 +7,6 @@ ) from attrs import define as _attrs_define -from attrs import field as _attrs_field from ..models.subscribe_mode import SubscribeMode from ..types import UNSET, Unset @@ -30,7 +29,6 @@ class PeerOptionsAgent: output: Union[Unset, "AgentOutput"] = UNSET subscribe_mode: Union[Unset, SubscribeMode] = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: output: Union[Unset, dict[str, Any]] = UNSET @@ -42,7 +40,7 @@ def to_dict(self) -> dict[str, Any]: subscribe_mode = self.subscribe_mode.value field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({}) if output is not UNSET: field_dict["output"] = output @@ -75,21 +73,4 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: subscribe_mode=subscribe_mode, ) - peer_options_agent.additional_properties = d return peer_options_agent - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/fishjam/_openapi_client/models/peer_options_web_rtc.py b/fishjam/_openapi_client/models/peer_options_web_rtc.py index c3f4938..326cab1 100644 --- a/fishjam/_openapi_client/models/peer_options_web_rtc.py +++ b/fishjam/_openapi_client/models/peer_options_web_rtc.py @@ -7,7 +7,6 @@ ) from attrs import define as _attrs_define -from attrs import field as _attrs_field from ..models.subscribe_mode import SubscribeMode from ..types import UNSET, Unset @@ -30,7 +29,6 @@ class PeerOptionsWebRTC: metadata: Union[Unset, "WebRTCMetadata"] = UNSET subscribe_mode: Union[Unset, SubscribeMode] = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: metadata: Union[Unset, dict[str, Any]] = UNSET @@ -42,7 +40,7 @@ def to_dict(self) -> dict[str, Any]: subscribe_mode = self.subscribe_mode.value field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({}) if metadata is not UNSET: field_dict["metadata"] = metadata @@ -75,21 +73,4 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: subscribe_mode=subscribe_mode, ) - peer_options_web_rtc.additional_properties = d return peer_options_web_rtc - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/fishjam/_openapi_client/models/room_config.py b/fishjam/_openapi_client/models/room_config.py index 8ba15a7..1342a86 100644 --- a/fishjam/_openapi_client/models/room_config.py +++ b/fishjam/_openapi_client/models/room_config.py @@ -7,7 +7,6 @@ ) from attrs import define as _attrs_define -from attrs import field as _attrs_field from ..models.room_type import RoomType from ..models.video_codec import VideoCodec @@ -34,7 +33,6 @@ class RoomConfig: room_type: Union[Unset, RoomType] = UNSET video_codec: Union[Unset, VideoCodec] = UNSET webhook_url: Union[None, Unset, str] = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: max_peers: Union[None, Unset, int] @@ -60,7 +58,7 @@ def to_dict(self) -> dict[str, Any]: webhook_url = self.webhook_url field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({}) if max_peers is not UNSET: field_dict["maxPeers"] = max_peers @@ -121,21 +119,4 @@ def _parse_webhook_url(data: object) -> Union[None, Unset, str]: webhook_url=webhook_url, ) - room_config.additional_properties = d return room_config - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/fishjam/_openapi_client/models/stream_config.py b/fishjam/_openapi_client/models/stream_config.py index 1697925..8351355 100644 --- a/fishjam/_openapi_client/models/stream_config.py +++ b/fishjam/_openapi_client/models/stream_config.py @@ -7,7 +7,6 @@ ) from attrs import define as _attrs_define -from attrs import field as _attrs_field from ..types import UNSET, Unset @@ -21,11 +20,12 @@ class StreamConfig: Attributes: audio_only (Union[None, Unset, bool]): Restrics stream to audio only Default: False. public (Union[Unset, bool]): True if livestream viewers can omit specifying a token. Default: False. + webhook_url (Union[None, Unset, str]): Webhook URL for receiving server notifications """ audio_only: Union[None, Unset, bool] = False public: Union[Unset, bool] = False - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + webhook_url: Union[None, Unset, str] = UNSET def to_dict(self) -> dict[str, Any]: audio_only: Union[None, Unset, bool] @@ -36,13 +36,21 @@ def to_dict(self) -> dict[str, Any]: public = self.public + webhook_url: Union[None, Unset, str] + if isinstance(self.webhook_url, Unset): + webhook_url = UNSET + else: + webhook_url = self.webhook_url + field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({}) if audio_only is not UNSET: field_dict["audioOnly"] = audio_only if public is not UNSET: field_dict["public"] = public + if webhook_url is not UNSET: + field_dict["webhookUrl"] = webhook_url return field_dict @@ -61,26 +69,19 @@ def _parse_audio_only(data: object) -> Union[None, Unset, bool]: public = d.pop("public", UNSET) + def _parse_webhook_url(data: object) -> Union[None, Unset, str]: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(Union[None, Unset, str], data) + + webhook_url = _parse_webhook_url(d.pop("webhookUrl", UNSET)) + stream_config = cls( audio_only=audio_only, public=public, + webhook_url=webhook_url, ) - stream_config.additional_properties = d return stream_config - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/fishjam/_ws_notifier.py b/fishjam/_ws_notifier.py index b3c2696..bfbd2da 100644 --- a/fishjam/_ws_notifier.py +++ b/fishjam/_ws_notifier.py @@ -130,7 +130,7 @@ async def _receive_loop(self): raise RuntimeError("Notification handler is not defined") while True: - message = cast(bytes, await self._websocket.recv()) + message = await self._websocket.recv(decode=False) message = ServerMessage().parse(message) _which, message = betterproto.which_one_of(message, "content") diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index be0c021..3dc5d29 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -19,11 +19,11 @@ generate_viewer_token as viewer_generate_viewer_token, ) from fishjam._openapi_client.models import ( - AddPeerBody, AgentOutput, AudioFormat, AudioSampleRate, Peer, + PeerConfig, PeerDetailsResponse, PeerOptionsAgent, PeerOptionsWebRTC, @@ -177,7 +177,7 @@ def create_peer( metadata=peer_metadata, subscribe_mode=SubscribeMode(options.subscribe_mode), ) - body = AddPeerBody(type_=PeerType.WEBRTC, options=peer_options) + body = PeerConfig(type_=PeerType.WEBRTC, options=peer_options) resp = cast( PeerDetailsResponse, @@ -198,7 +198,7 @@ def create_agent(self, room_id: str, options: AgentOptions | None = None): and Fishjam URL. """ options = options or AgentOptions() - body = AddPeerBody( + body = PeerConfig( type_=PeerType.AGENT, options=PeerOptionsAgent( output=AgentOutput( diff --git a/protos b/protos index 40f4ab8..9d807b5 160000 --- a/protos +++ b/protos @@ -1 +1 @@ -Subproject commit 40f4ab8013644de2be5d7d7ff2652725935a2e92 +Subproject commit 9d807b55279de385136f82b12f5df75d73104514 diff --git a/pyproject.toml b/pyproject.toml index 1f8d304..4cdbcaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fishjam-server-sdk" -version = "0.24.0" +version = "0.25.0" description = "Python server SDK for the Fishjam" authors = [{ name = "Fishjam Team", email = "contact@fishjam.io" }] requires-python = ">=3.11" diff --git a/tests/Dockerfile b/tests/Dockerfile deleted file mode 100644 index 52c6349..0000000 --- a/tests/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -ARG UV_VERSION=0.8 -ARG PYTHON_VERSION=3.11 -FROM ghcr.io/astral-sh/uv:$UV_VERSION AS uv-image - -ARG PYTHON_VERSION -FROM python:$PYTHON_VERSION-slim AS builder - -COPY --from=uv-image /uv /uvx /bin/ - -WORKDIR /app - -COPY pyproject.toml . - -COPY examples/transcription/pyproject.toml ./examples/transcription/ -COPY examples/poet_chat/pyproject.toml ./examples/poet_chat/ -COPY examples/selective_subscription/pyproject.toml ./examples/selective_subscription/ - -COPY uv.lock . - -RUN uv sync --locked --no-install-project --all-extras - -COPY . /app diff --git a/tests/agent/test_agent.py b/tests/agent/test_agent.py index ad09984..d29f3a2 100644 --- a/tests/agent/test_agent.py +++ b/tests/agent/test_agent.py @@ -1,5 +1,4 @@ import asyncio -import os from contextlib import suppress import pytest @@ -14,15 +13,12 @@ ServerMessagePeerMetadataUpdated, ) from fishjam.events.allowed_notifications import AllowedNotification - -HOST = "proxy" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -FISHJAM_ID = f"http://{HOST}:5555" -SERVER_API_TOKEN = os.getenv("MANAGEMENT_TOKEN", "development") +from tests.support.env import FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN @pytest.fixture def room_api(): - return FishjamClient(FISHJAM_ID, SERVER_API_TOKEN) + return FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) @pytest.fixture @@ -43,7 +39,7 @@ def agent(room: Room, room_api: FishjamClient): async def notifier(): notifier = FishjamNotifier( fishjam_id=FISHJAM_ID, - management_token=SERVER_API_TOKEN, + management_token=FISHJAM_MANAGEMENT_TOKEN, ) @notifier.on_server_notification diff --git a/tests/support/asyncio_utils.py b/tests/support/asyncio_utils.py index e415766..92a4734 100644 --- a/tests/support/asyncio_utils.py +++ b/tests/support/asyncio_utils.py @@ -29,11 +29,3 @@ def handle_message(message): raise asyncio.exceptions.TimeoutError( f"{message_checks[0]} hasn't been received within timeout" ) from exc - - -async def cancel(task): - task.cancel() - try: - await task - except asyncio.exceptions.CancelledError: - pass diff --git a/tests/support/env.py b/tests/support/env.py new file mode 100644 index 0000000..b96d138 --- /dev/null +++ b/tests/support/env.py @@ -0,0 +1,6 @@ +import os + +FISHJAM_ID = os.environ["FISHJAM_ID"] +FISHJAM_MANAGEMENT_TOKEN = os.environ["FISHJAM_MANAGEMENT_TOKEN"] +WEBHOOK_SERVER_URL = os.getenv("WEBHOOK_SERVER_URL", "http://localhost:5000") +WEBHOOK_URL = f"{WEBHOOK_SERVER_URL}/webhook" diff --git a/tests/test_notifier.py b/tests/test_notifier.py index 7db7d5d..854dbc7 100644 --- a/tests/test_notifier.py +++ b/tests/test_notifier.py @@ -1,14 +1,13 @@ # pylint: disable=locally-disabled, missing-class-docstring, missing-function-docstring, redefined-outer-name, too-few-public-methods, missing-module-docstring import asyncio -import os -import socket -import time from multiprocessing import Process, Queue import pytest import requests import websockets +from requests.adapters import HTTPAdapter +from urllib3 import Retry from fishjam import FishjamClient, FishjamNotifier, RoomOptions from fishjam.events import ( @@ -19,16 +18,16 @@ ServerMessageRoomCreated, ServerMessageRoomDeleted, ) -from tests.support.asyncio_utils import assert_events, cancel +from tests.support.asyncio_utils import assert_events +from tests.support.env import ( + FISHJAM_ID, + FISHJAM_MANAGEMENT_TOKEN, + WEBHOOK_SERVER_URL, + WEBHOOK_URL, +) from tests.support.peer_socket import PeerSocket from tests.support.webhook_notifier import run_server -FISHJAM_HOST = "proxy" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -FISHJAM_URL = f"http://{FISHJAM_HOST}:5555" -FISHJAM_ID = FISHJAM_URL -SERVER_API_TOKEN = os.getenv("MANAGEMENT_TOKEN", "development") -WEBHOOK_ADDRESS = "test" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -WEBHOOK_URL = f"http://{WEBHOOK_ADDRESS}:5000/webhook" queue = Queue() @@ -37,19 +36,21 @@ def start_server(): flask_process = Process(target=run_server, args=(queue,)) flask_process.start() - timeout = 60 # wait maximum of 60 seconds - while True: - try: - response = requests.get(f"http://{WEBHOOK_ADDRESS}:5000/", timeout=1_000) - if response.status_code == 200: # Or another condition - break - except (requests.ConnectionError, socket.error): - time.sleep(1) # wait for 1 second before trying again - timeout -= 1 - if timeout == 0: - pytest.fail("Server did not start in the expected time") + session = requests.Session() + session.mount( + "http", + HTTPAdapter( + max_retries=Retry( + total=5, + backoff_factor=0.25, + ) + ), + ) + + response = session.get(WEBHOOK_SERVER_URL, timeout=5) + response.raise_for_status() - yield # This is where the testing happens. + yield flask_process.terminate() @@ -59,32 +60,35 @@ class TestConnectingToServer: async def test_valid_credentials(self): notifier = FishjamNotifier( fishjam_id=FISHJAM_ID, - management_token=SERVER_API_TOKEN, + management_token=FISHJAM_MANAGEMENT_TOKEN, ) @notifier.on_server_notification def handle_notitifcation(_notification): pass - notifier_task = asyncio.create_task(notifier.connect()) - await notifier.wait_ready() - assert ( - notifier._websocket and notifier._websocket.state == websockets.State.OPEN - ) + async with asyncio.TaskGroup() as tg: + notifier_task = tg.create_task(notifier.connect()) + await notifier.wait_ready() + + assert ( + notifier._websocket + and notifier._websocket.state == websockets.State.OPEN + ) - await cancel(notifier_task) + notifier_task.cancel() @pytest.fixture def room_api(): - return FishjamClient(FISHJAM_ID, SERVER_API_TOKEN) + return FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) @pytest.fixture def notifier(): notifier = FishjamNotifier( fishjam_id=FISHJAM_ID, - management_token=SERVER_API_TOKEN, + management_token=FISHJAM_MANAGEMENT_TOKEN, ) return notifier @@ -96,18 +100,21 @@ async def test_room_created_deleted( self, room_api: FishjamClient, notifier: FishjamNotifier ): event_checks = [ServerMessageRoomCreated, ServerMessageRoomDeleted] - assert_task = asyncio.create_task(assert_events(notifier, event_checks.copy())) - notifier_task = asyncio.create_task(notifier.connect()) - await notifier.wait_ready() + async with asyncio.TaskGroup() as tg: + assert_task = tg.create_task(assert_events(notifier, event_checks.copy())) + + notifier_task = tg.create_task(notifier.connect()) + await notifier.wait_ready() - options = RoomOptions(webhook_url=WEBHOOK_URL) - room = room_api.create_room(options=options) + options = RoomOptions(webhook_url=WEBHOOK_URL) + room = room_api.create_room(options=options) - room_api.delete_room(room.id) + room_api.delete_room(room.id) - await assert_task - await cancel(notifier_task) + await assert_task + + notifier_task.cancel() for event in event_checks: self.assert_event(event) @@ -124,26 +131,29 @@ async def test_peer_connected_disconnected( ServerMessagePeerDeleted, ServerMessageRoomDeleted, ] - assert_task = asyncio.create_task(assert_events(notifier, event_checks.copy())) - notifier_task = asyncio.create_task(notifier.connect()) - await notifier.wait_ready() + async with asyncio.TaskGroup() as tg: + assert_task = tg.create_task(assert_events(notifier, event_checks.copy())) + + notifier_task = tg.create_task(notifier.connect()) + await notifier.wait_ready() + + options = RoomOptions(webhook_url=WEBHOOK_URL) + room = room_api.create_room(options=options) - options = RoomOptions(webhook_url=WEBHOOK_URL) - room = room_api.create_room(options=options) + peer, token = room_api.create_peer(room.id) + peer_socket = PeerSocket(fishjam_url=FISHJAM_ID) + peer_socket_task = tg.create_task(peer_socket.connect(token)) - peer, token = room_api.create_peer(room.id) - peer_socket = PeerSocket(fishjam_url=FISHJAM_URL) - peer_task = asyncio.create_task(peer_socket.connect(token)) + await peer_socket.wait_ready() - await peer_socket.wait_ready() + room_api.delete_peer(room.id, peer.id) + room_api.delete_room(room.id) - room_api.delete_peer(room.id, peer.id) - room_api.delete_room(room.id) + await assert_task - await assert_task - await cancel(peer_task) - await cancel(notifier_task) + notifier_task.cancel() + peer_socket_task.cancel() for event in event_checks: self.assert_event(event) @@ -159,29 +169,32 @@ async def test_peer_connected_room_deleted( ServerMessagePeerDeleted, ServerMessageRoomDeleted, ] - assert_task = asyncio.create_task(assert_events(notifier, event_checks.copy())) - notifier_task = asyncio.create_task(notifier.connect()) - await notifier.wait_ready() + async with asyncio.TaskGroup() as tg: + assert_task = tg.create_task(assert_events(notifier, event_checks.copy())) + + notifier_task = tg.create_task(notifier.connect()) + await notifier.wait_ready() + + options = RoomOptions(webhook_url=WEBHOOK_URL) + room = room_api.create_room(options=options) + _peer, token = room_api.create_peer(room.id) - options = RoomOptions(webhook_url=WEBHOOK_URL) - room = room_api.create_room(options=options) - _peer, token = room_api.create_peer(room.id) + peer_socket = PeerSocket(fishjam_url=FISHJAM_ID) + peer_socket_task = tg.create_task(peer_socket.connect(token)) - peer_socket = PeerSocket(fishjam_url=FISHJAM_URL) - peer_task = asyncio.create_task(peer_socket.connect(token)) + await peer_socket.wait_ready() - await peer_socket.wait_ready() + room_api.delete_room(room.id) - room_api.delete_room(room.id) + await assert_task - await assert_task - await cancel(peer_task) - await cancel(notifier_task) + notifier_task.cancel() + peer_socket_task.cancel() for event in event_checks: self.assert_event(event) def assert_event(self, event): - data = queue.get(timeout=2.5) + data = queue.get(timeout=5) assert data == event or isinstance(data, event) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 72f9c85..3407e1e 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -1,4 +1,3 @@ -import os from unittest.mock import Mock, patch import httpx @@ -29,10 +28,7 @@ VideoCodec, ) from fishjam.version import get_version - -HOST = "proxy" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" -FISHJAM_ID = f"http://{HOST}:5555" -MANAGEMENT_TOKEN = os.getenv("MANAGEMENT_TOKEN", "development") +from tests.support.env import FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN MAX_PEERS = 10 CODEC_H264 = "h264" @@ -49,7 +45,7 @@ def test_invalid_token(self): room_api.create_room() def test_valid_token(self): - room_api = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) + room_api = FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) room = room_api.create_room() all_rooms = room_api.get_all_rooms() @@ -71,7 +67,7 @@ def mock_send(request, **kwargs): captured_headers = dict(request.headers) return mock_response - room_api = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) + room_api = FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) with patch.object(httpx.HTTPTransport, "handle_request", side_effect=mock_send): try: @@ -89,7 +85,7 @@ def mock_send(request, **kwargs): @pytest.fixture def room_api(): - return FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) + return FishjamClient(FISHJAM_ID, FISHJAM_MANAGEMENT_TOKEN) class TestCreateRoom: diff --git a/uv.lock b/uv.lock index 92efb07..56ea2ae 100644 --- a/uv.lock +++ b/uv.lock @@ -322,7 +322,7 @@ wheels = [ [[package]] name = "fishjam-server-sdk" -version = "0.24.0" +version = "0.25.0" source = { editable = "." } dependencies = [ { name = "aenum" },