From d28d86e8a1c3a37d0fbdc4a5ae3af01a9ec21ce0 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 24 Aug 2026 13:23:04 +0200 Subject: [PATCH 1/7] Add the generated Composition API client Generated from the Composition API spec, the same way as the Fishjam client. The whip and whep endpoints speak application/sdp, which the generator does not support, so they are absent. --- fishjam/_composition_client/__init__.py | 8 + fishjam/_composition_client/api/__init__.py | 1 + .../api/compositions/__init__.py | 1 + .../api/compositions/create_composition.py | 186 +++++ .../api/compositions/delete_composition.py | 168 ++++ .../api/compositions/reset.py | 170 ++++ .../api/compositions/start.py | 170 ++++ .../api/events/__init__.py | 1 + .../api/events/send_composition_event.py | 197 +++++ .../api/inputs/__init__.py | 1 + .../api/inputs/register_input.py | 226 ++++++ .../api/inputs/unregister_input.py | 216 ++++++ .../api/media_transport/__init__.py | 1 + .../api/outputs/__init__.py | 1 + .../api/outputs/register_output.py | 220 ++++++ .../api/outputs/register_template_output.py | 214 ++++++ .../api/outputs/request_keyframe.py | 189 +++++ .../api/outputs/unregister_output.py | 216 ++++++ .../api/outputs/update_output.py | 216 ++++++ .../api/renderers/__init__.py | 1 + .../api/renderers/register_font.py | 195 +++++ .../api/renderers/register_image.py | 229 ++++++ .../api/renderers/unregister_image.py | 216 ++++++ fishjam/_composition_client/client.py | 282 +++++++ fishjam/_composition_client/errors.py | 16 + .../_composition_client/models/__init__.py | 177 +++++ .../_composition_client/models/api_error.py | 59 ++ .../models/audio_channels.py | 11 + .../models/audio_mixing_strategy.py | 11 + .../_composition_client/models/audio_scene.py | 54 ++ .../models/audio_scene_input.py | 62 ++ .../models/average_and_max_bitrate.py | 69 ++ .../_composition_client/models/box_shadow.py | 114 +++ .../models/composition_created_response.py | 67 ++ .../models/create_composition_request.py | 56 ++ .../models/easing_function_bounce.py | 63 ++ .../easing_function_bounce_function_name.py | 10 + .../models/easing_function_cubic_bezier.py | 71 ++ ...ing_function_cubic_bezier_function_name.py | 10 + .../models/easing_function_linear.py | 63 ++ .../easing_function_linear_function_name.py | 10 + .../models/empty_response.py | 46 ++ .../_composition_client/models/font_upload.py | 62 ++ .../models/h264_encoder_preset.py | 19 + .../models/horizontal_align.py | 13 + fishjam/_composition_client/models/image.py | 115 +++ .../models/image_spec_auto.py | 69 ++ .../models/image_spec_auto_asset_type.py | 10 + .../models/image_spec_gif.py | 69 ++ .../models/image_spec_gif_asset_type.py | 10 + .../models/image_spec_jpeg.py | 69 ++ .../models/image_spec_jpeg_asset_type.py | 10 + .../models/image_spec_png.py | 69 ++ .../models/image_spec_png_asset_type.py | 10 + .../models/image_spec_svg.py | 108 +++ .../models/image_spec_svg_asset_type.py | 10 + .../_composition_client/models/image_type.py | 10 + .../models/input_stream.py | 71 ++ .../models/input_stream_type.py | 10 + .../models/interpolation.py | 11 + .../_composition_client/models/mp_4_input.py | 73 ++ .../models/mp_4_input_type.py | 10 + .../models/opus_encoder_preset.py | 12 + .../models/output_end_condition.py | 147 ++++ .../output_rtmp_client_audio_options.py | 144 ++++ .../output_rtmp_client_video_options.py | 92 +++ .../models/output_whip_audio_options.py | 241 ++++++ .../models/output_whip_video_options.py | 92 +++ .../_composition_client/models/overflow.py | 12 + .../models/pixel_format.py | 12 + .../models/register_font_body.py | 73 ++ .../models/register_input_response.py | 134 ++++ .../models/register_template_output.py | 100 +++ .../models/register_template_output_body.py | 125 +++ .../models/rescale_mode.py | 11 + .../_composition_client/models/rescaler.py | 544 +++++++++++++ .../models/rescaler_type.py | 10 + .../_composition_client/models/resolution.py | 67 ++ .../_composition_client/models/rtmp_input.py | 52 ++ .../models/rtmp_input_type.py | 10 + .../_composition_client/models/rtmp_output.py | 130 ++++ .../models/rtmp_output_type.py | 10 + .../models/send_composition_event_body.py | 51 ++ fishjam/_composition_client/models/text.py | 375 +++++++++ .../_composition_client/models/text_style.py | 12 + .../_composition_client/models/text_type.py | 10 + .../_composition_client/models/text_weight.py | 18 + .../models/text_wrap_mode.py | 12 + fishjam/_composition_client/models/tiles.py | 425 ++++++++++ .../_composition_client/models/tiles_type.py | 10 + .../_composition_client/models/transition.py | 174 +++++ .../models/transport_protocol.py | 11 + .../models/unregister_input.py | 75 ++ .../models/unregister_output.py | 75 ++ .../models/unregister_renderer.py | 75 ++ .../models/update_output_request.py | 125 +++ .../models/vertical_align.py | 13 + .../_composition_client/models/video_scene.py | 124 +++ fishjam/_composition_client/models/view.py | 723 ++++++++++++++++++ .../models/view_direction.py | 11 + .../_composition_client/models/view_type.py | 10 + .../_composition_client/models/whep_input.py | 92 +++ .../models/whep_input_type.py | 10 + .../models/whip_audio_encoder_options_any.py | 61 ++ .../whip_audio_encoder_options_any_type.py | 10 + .../models/whip_audio_encoder_options_opus.py | 139 ++++ .../whip_audio_encoder_options_opus_type.py | 10 + .../_composition_client/models/whip_input.py | 86 +++ .../models/whip_input_type.py | 10 + .../_composition_client/models/whip_output.py | 142 ++++ .../models/whip_output_type.py | 10 + fishjam/_composition_client/types.py | 54 ++ openapi-python-client-composition-config.yaml | 2 + pyproject.toml | 3 + scripts.py | 28 +- 115 files changed, 10121 insertions(+), 5 deletions(-) create mode 100644 fishjam/_composition_client/__init__.py create mode 100644 fishjam/_composition_client/api/__init__.py create mode 100644 fishjam/_composition_client/api/compositions/__init__.py create mode 100644 fishjam/_composition_client/api/compositions/create_composition.py create mode 100644 fishjam/_composition_client/api/compositions/delete_composition.py create mode 100644 fishjam/_composition_client/api/compositions/reset.py create mode 100644 fishjam/_composition_client/api/compositions/start.py create mode 100644 fishjam/_composition_client/api/events/__init__.py create mode 100644 fishjam/_composition_client/api/events/send_composition_event.py create mode 100644 fishjam/_composition_client/api/inputs/__init__.py create mode 100644 fishjam/_composition_client/api/inputs/register_input.py create mode 100644 fishjam/_composition_client/api/inputs/unregister_input.py create mode 100644 fishjam/_composition_client/api/media_transport/__init__.py create mode 100644 fishjam/_composition_client/api/outputs/__init__.py create mode 100644 fishjam/_composition_client/api/outputs/register_output.py create mode 100644 fishjam/_composition_client/api/outputs/register_template_output.py create mode 100644 fishjam/_composition_client/api/outputs/request_keyframe.py create mode 100644 fishjam/_composition_client/api/outputs/unregister_output.py create mode 100644 fishjam/_composition_client/api/outputs/update_output.py create mode 100644 fishjam/_composition_client/api/renderers/__init__.py create mode 100644 fishjam/_composition_client/api/renderers/register_font.py create mode 100644 fishjam/_composition_client/api/renderers/register_image.py create mode 100644 fishjam/_composition_client/api/renderers/unregister_image.py create mode 100644 fishjam/_composition_client/client.py create mode 100644 fishjam/_composition_client/errors.py create mode 100644 fishjam/_composition_client/models/__init__.py create mode 100644 fishjam/_composition_client/models/api_error.py create mode 100644 fishjam/_composition_client/models/audio_channels.py create mode 100644 fishjam/_composition_client/models/audio_mixing_strategy.py create mode 100644 fishjam/_composition_client/models/audio_scene.py create mode 100644 fishjam/_composition_client/models/audio_scene_input.py create mode 100644 fishjam/_composition_client/models/average_and_max_bitrate.py create mode 100644 fishjam/_composition_client/models/box_shadow.py create mode 100644 fishjam/_composition_client/models/composition_created_response.py create mode 100644 fishjam/_composition_client/models/create_composition_request.py create mode 100644 fishjam/_composition_client/models/easing_function_bounce.py create mode 100644 fishjam/_composition_client/models/easing_function_bounce_function_name.py create mode 100644 fishjam/_composition_client/models/easing_function_cubic_bezier.py create mode 100644 fishjam/_composition_client/models/easing_function_cubic_bezier_function_name.py create mode 100644 fishjam/_composition_client/models/easing_function_linear.py create mode 100644 fishjam/_composition_client/models/easing_function_linear_function_name.py create mode 100644 fishjam/_composition_client/models/empty_response.py create mode 100644 fishjam/_composition_client/models/font_upload.py create mode 100644 fishjam/_composition_client/models/h264_encoder_preset.py create mode 100644 fishjam/_composition_client/models/horizontal_align.py create mode 100644 fishjam/_composition_client/models/image.py create mode 100644 fishjam/_composition_client/models/image_spec_auto.py create mode 100644 fishjam/_composition_client/models/image_spec_auto_asset_type.py create mode 100644 fishjam/_composition_client/models/image_spec_gif.py create mode 100644 fishjam/_composition_client/models/image_spec_gif_asset_type.py create mode 100644 fishjam/_composition_client/models/image_spec_jpeg.py create mode 100644 fishjam/_composition_client/models/image_spec_jpeg_asset_type.py create mode 100644 fishjam/_composition_client/models/image_spec_png.py create mode 100644 fishjam/_composition_client/models/image_spec_png_asset_type.py create mode 100644 fishjam/_composition_client/models/image_spec_svg.py create mode 100644 fishjam/_composition_client/models/image_spec_svg_asset_type.py create mode 100644 fishjam/_composition_client/models/image_type.py create mode 100644 fishjam/_composition_client/models/input_stream.py create mode 100644 fishjam/_composition_client/models/input_stream_type.py create mode 100644 fishjam/_composition_client/models/interpolation.py create mode 100644 fishjam/_composition_client/models/mp_4_input.py create mode 100644 fishjam/_composition_client/models/mp_4_input_type.py create mode 100644 fishjam/_composition_client/models/opus_encoder_preset.py create mode 100644 fishjam/_composition_client/models/output_end_condition.py create mode 100644 fishjam/_composition_client/models/output_rtmp_client_audio_options.py create mode 100644 fishjam/_composition_client/models/output_rtmp_client_video_options.py create mode 100644 fishjam/_composition_client/models/output_whip_audio_options.py create mode 100644 fishjam/_composition_client/models/output_whip_video_options.py create mode 100644 fishjam/_composition_client/models/overflow.py create mode 100644 fishjam/_composition_client/models/pixel_format.py create mode 100644 fishjam/_composition_client/models/register_font_body.py create mode 100644 fishjam/_composition_client/models/register_input_response.py create mode 100644 fishjam/_composition_client/models/register_template_output.py create mode 100644 fishjam/_composition_client/models/register_template_output_body.py create mode 100644 fishjam/_composition_client/models/rescale_mode.py create mode 100644 fishjam/_composition_client/models/rescaler.py create mode 100644 fishjam/_composition_client/models/rescaler_type.py create mode 100644 fishjam/_composition_client/models/resolution.py create mode 100644 fishjam/_composition_client/models/rtmp_input.py create mode 100644 fishjam/_composition_client/models/rtmp_input_type.py create mode 100644 fishjam/_composition_client/models/rtmp_output.py create mode 100644 fishjam/_composition_client/models/rtmp_output_type.py create mode 100644 fishjam/_composition_client/models/send_composition_event_body.py create mode 100644 fishjam/_composition_client/models/text.py create mode 100644 fishjam/_composition_client/models/text_style.py create mode 100644 fishjam/_composition_client/models/text_type.py create mode 100644 fishjam/_composition_client/models/text_weight.py create mode 100644 fishjam/_composition_client/models/text_wrap_mode.py create mode 100644 fishjam/_composition_client/models/tiles.py create mode 100644 fishjam/_composition_client/models/tiles_type.py create mode 100644 fishjam/_composition_client/models/transition.py create mode 100644 fishjam/_composition_client/models/transport_protocol.py create mode 100644 fishjam/_composition_client/models/unregister_input.py create mode 100644 fishjam/_composition_client/models/unregister_output.py create mode 100644 fishjam/_composition_client/models/unregister_renderer.py create mode 100644 fishjam/_composition_client/models/update_output_request.py create mode 100644 fishjam/_composition_client/models/vertical_align.py create mode 100644 fishjam/_composition_client/models/video_scene.py create mode 100644 fishjam/_composition_client/models/view.py create mode 100644 fishjam/_composition_client/models/view_direction.py create mode 100644 fishjam/_composition_client/models/view_type.py create mode 100644 fishjam/_composition_client/models/whep_input.py create mode 100644 fishjam/_composition_client/models/whep_input_type.py create mode 100644 fishjam/_composition_client/models/whip_audio_encoder_options_any.py create mode 100644 fishjam/_composition_client/models/whip_audio_encoder_options_any_type.py create mode 100644 fishjam/_composition_client/models/whip_audio_encoder_options_opus.py create mode 100644 fishjam/_composition_client/models/whip_audio_encoder_options_opus_type.py create mode 100644 fishjam/_composition_client/models/whip_input.py create mode 100644 fishjam/_composition_client/models/whip_input_type.py create mode 100644 fishjam/_composition_client/models/whip_output.py create mode 100644 fishjam/_composition_client/models/whip_output_type.py create mode 100644 fishjam/_composition_client/types.py create mode 100644 openapi-python-client-composition-config.yaml diff --git a/fishjam/_composition_client/__init__.py b/fishjam/_composition_client/__init__.py new file mode 100644 index 0000000..44f789d --- /dev/null +++ b/fishjam/_composition_client/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing Composition API""" + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/fishjam/_composition_client/api/__init__.py b/fishjam/_composition_client/api/__init__.py new file mode 100644 index 0000000..81f9fa2 --- /dev/null +++ b/fishjam/_composition_client/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/fishjam/_composition_client/api/compositions/__init__.py b/fishjam/_composition_client/api/compositions/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_composition_client/api/compositions/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_composition_client/api/compositions/create_composition.py b/fishjam/_composition_client/api/compositions/create_composition.py new file mode 100644 index 0000000..10ba341 --- /dev/null +++ b/fishjam/_composition_client/api/compositions/create_composition.py @@ -0,0 +1,186 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.composition_created_response import CompositionCreatedResponse +from ...models.create_composition_request import CreateCompositionRequest +from ...types import Response + + +def _get_kwargs( + *, + body: CreateCompositionRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | CompositionCreatedResponse | None: + if response.status_code == 201: + response_201 = CompositionCreatedResponse.from_dict(response.json()) + + return response_201 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if response.status_code == 503: + response_503 = ApiError.from_dict(response.json()) + + return response_503 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | CompositionCreatedResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: CreateCompositionRequest, +) -> Response[ApiError | CompositionCreatedResponse]: + """Create a composition + + Args: + body (CreateCompositionRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | CompositionCreatedResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: CreateCompositionRequest, +) -> ApiError | CompositionCreatedResponse | None: + """Create a composition + + Args: + body (CreateCompositionRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | CompositionCreatedResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: CreateCompositionRequest, +) -> Response[ApiError | CompositionCreatedResponse]: + """Create a composition + + Args: + body (CreateCompositionRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | CompositionCreatedResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: CreateCompositionRequest, +) -> ApiError | CompositionCreatedResponse | None: + """Create a composition + + Args: + body (CreateCompositionRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | CompositionCreatedResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/compositions/delete_composition.py b/fishjam/_composition_client/api/compositions/delete_composition.py new file mode 100644 index 0000000..611a652 --- /dev/null +++ b/fishjam/_composition_client/api/compositions/delete_composition.py @@ -0,0 +1,168 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...types import Response + + +def _get_kwargs( + composition_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "delete", + "url": "/api/composition/{composition_id}".format( + composition_id=quote(str(composition_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | ApiError | None: + if response.status_code == 200: + response_200 = cast(Any, None) + return response_200 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any | ApiError]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | ApiError]: + """Delete a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ApiError] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Any | ApiError | None: + """Delete a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ApiError + """ + + return sync_detailed( + composition_id=composition_id, + client=client, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Response[Any | ApiError]: + """Delete a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ApiError] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Any | ApiError | None: + """Delete a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ApiError + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + client=client, + ) + ).parsed diff --git a/fishjam/_composition_client/api/compositions/reset.py b/fishjam/_composition_client/api/compositions/reset.py new file mode 100644 index 0000000..57d81de --- /dev/null +++ b/fishjam/_composition_client/api/compositions/reset.py @@ -0,0 +1,170 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...types import Response + + +def _get_kwargs( + composition_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/reset".format( + composition_id=quote(str(composition_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Response[ApiError | EmptyResponse]: + """Reset a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + *, + client: AuthenticatedClient, +) -> ApiError | EmptyResponse | None: + """Reset a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + client=client, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Response[ApiError | EmptyResponse]: + """Reset a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + *, + client: AuthenticatedClient, +) -> ApiError | EmptyResponse | None: + """Reset a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + client=client, + ) + ).parsed diff --git a/fishjam/_composition_client/api/compositions/start.py b/fishjam/_composition_client/api/compositions/start.py new file mode 100644 index 0000000..901ce84 --- /dev/null +++ b/fishjam/_composition_client/api/compositions/start.py @@ -0,0 +1,170 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...types import Response + + +def _get_kwargs( + composition_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/start".format( + composition_id=quote(str(composition_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Response[ApiError | EmptyResponse]: + """Start a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + *, + client: AuthenticatedClient, +) -> ApiError | EmptyResponse | None: + """Start a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + client=client, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + *, + client: AuthenticatedClient, +) -> Response[ApiError | EmptyResponse]: + """Start a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + *, + client: AuthenticatedClient, +) -> ApiError | EmptyResponse | None: + """Start a composition + + Args: + composition_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + client=client, + ) + ).parsed diff --git a/fishjam/_composition_client/api/events/__init__.py b/fishjam/_composition_client/api/events/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_composition_client/api/events/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_composition_client/api/events/send_composition_event.py b/fishjam/_composition_client/api/events/send_composition_event.py new file mode 100644 index 0000000..823f285 --- /dev/null +++ b/fishjam/_composition_client/api/events/send_composition_event.py @@ -0,0 +1,197 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.send_composition_event_body import SendCompositionEventBody +from ...types import Response + + +def _get_kwargs( + composition_id: str, + *, + body: SendCompositionEventBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/event".format( + composition_id=quote(str(composition_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 503: + response_503 = ApiError.from_dict(response.json()) + + return response_503 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + *, + client: AuthenticatedClient, + body: SendCompositionEventBody, +) -> Response[ApiError | EmptyResponse]: + """Send an event to templates + + Args: + composition_id (str): + body (SendCompositionEventBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + *, + client: AuthenticatedClient, + body: SendCompositionEventBody, +) -> ApiError | EmptyResponse | None: + """Send an event to templates + + Args: + composition_id (str): + body (SendCompositionEventBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + *, + client: AuthenticatedClient, + body: SendCompositionEventBody, +) -> Response[ApiError | EmptyResponse]: + """Send an event to templates + + Args: + composition_id (str): + body (SendCompositionEventBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + *, + client: AuthenticatedClient, + body: SendCompositionEventBody, +) -> ApiError | EmptyResponse | None: + """Send an event to templates + + Args: + composition_id (str): + body (SendCompositionEventBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/inputs/__init__.py b/fishjam/_composition_client/api/inputs/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_composition_client/api/inputs/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_composition_client/api/inputs/register_input.py b/fishjam/_composition_client/api/inputs/register_input.py new file mode 100644 index 0000000..263486d --- /dev/null +++ b/fishjam/_composition_client/api/inputs/register_input.py @@ -0,0 +1,226 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.mp_4_input import Mp4Input +from ...models.register_input_response import RegisterInputResponse +from ...models.rtmp_input import RtmpInput +from ...models.whep_input import WhepInput +from ...models.whip_input import WhipInput +from ...types import Response + + +def _get_kwargs( + composition_id: str, + input_id: str, + *, + body: Mp4Input | RtmpInput | WhepInput | WhipInput, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/input/{input_id}/register".format( + composition_id=quote(str(composition_id), safe=""), + input_id=quote(str(input_id), safe=""), + ), + } + + if isinstance(body, RtmpInput): + _kwargs["json"] = body.to_dict() + elif isinstance(body, Mp4Input): + _kwargs["json"] = body.to_dict() + elif isinstance(body, WhipInput): + _kwargs["json"] = body.to_dict() + else: + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | RegisterInputResponse | None: + if response.status_code == 200: + response_200 = RegisterInputResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | RegisterInputResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: Mp4Input | RtmpInput | WhepInput | WhipInput, +) -> Response[ApiError | RegisterInputResponse]: + """Register an input + + Args: + composition_id (str): + input_id (str): + body (Mp4Input | RtmpInput | WhepInput | WhipInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | RegisterInputResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + input_id=input_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: Mp4Input | RtmpInput | WhepInput | WhipInput, +) -> ApiError | RegisterInputResponse | None: + """Register an input + + Args: + composition_id (str): + input_id (str): + body (Mp4Input | RtmpInput | WhepInput | WhipInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | RegisterInputResponse + """ + + return sync_detailed( + composition_id=composition_id, + input_id=input_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: Mp4Input | RtmpInput | WhepInput | WhipInput, +) -> Response[ApiError | RegisterInputResponse]: + """Register an input + + Args: + composition_id (str): + input_id (str): + body (Mp4Input | RtmpInput | WhepInput | WhipInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | RegisterInputResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + input_id=input_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: Mp4Input | RtmpInput | WhepInput | WhipInput, +) -> ApiError | RegisterInputResponse | None: + """Register an input + + Args: + composition_id (str): + input_id (str): + body (Mp4Input | RtmpInput | WhepInput | WhipInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | RegisterInputResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + input_id=input_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/inputs/unregister_input.py b/fishjam/_composition_client/api/inputs/unregister_input.py new file mode 100644 index 0000000..99583a8 --- /dev/null +++ b/fishjam/_composition_client/api/inputs/unregister_input.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.unregister_input import UnregisterInput +from ...types import Response + + +def _get_kwargs( + composition_id: str, + input_id: str, + *, + body: UnregisterInput, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/input/{input_id}/unregister".format( + composition_id=quote(str(composition_id), safe=""), + input_id=quote(str(input_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: UnregisterInput, +) -> Response[ApiError | EmptyResponse]: + """Unregister an input + + Args: + composition_id (str): + input_id (str): + body (UnregisterInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + input_id=input_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: UnregisterInput, +) -> ApiError | EmptyResponse | None: + """Unregister an input + + Args: + composition_id (str): + input_id (str): + body (UnregisterInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + input_id=input_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: UnregisterInput, +) -> Response[ApiError | EmptyResponse]: + """Unregister an input + + Args: + composition_id (str): + input_id (str): + body (UnregisterInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + input_id=input_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + input_id: str, + *, + client: AuthenticatedClient, + body: UnregisterInput, +) -> ApiError | EmptyResponse | None: + """Unregister an input + + Args: + composition_id (str): + input_id (str): + body (UnregisterInput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + input_id=input_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/media_transport/__init__.py b/fishjam/_composition_client/api/media_transport/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_composition_client/api/media_transport/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_composition_client/api/outputs/__init__.py b/fishjam/_composition_client/api/outputs/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_composition_client/api/outputs/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_composition_client/api/outputs/register_output.py b/fishjam/_composition_client/api/outputs/register_output.py new file mode 100644 index 0000000..58bb1fa --- /dev/null +++ b/fishjam/_composition_client/api/outputs/register_output.py @@ -0,0 +1,220 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.rtmp_output import RtmpOutput +from ...models.whip_output import WhipOutput +from ...types import Response + + +def _get_kwargs( + composition_id: str, + output_id: str, + *, + body: RtmpOutput | WhipOutput, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/output/{output_id}/register".format( + composition_id=quote(str(composition_id), safe=""), + output_id=quote(str(output_id), safe=""), + ), + } + + if isinstance(body, RtmpOutput): + _kwargs["json"] = body.to_dict() + else: + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RtmpOutput | WhipOutput, +) -> Response[ApiError | EmptyResponse]: + """Register an output + + Args: + composition_id (str): + output_id (str): + body (RtmpOutput | WhipOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RtmpOutput | WhipOutput, +) -> ApiError | EmptyResponse | None: + """Register an output + + Args: + composition_id (str): + output_id (str): + body (RtmpOutput | WhipOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RtmpOutput | WhipOutput, +) -> Response[ApiError | EmptyResponse]: + """Register an output + + Args: + composition_id (str): + output_id (str): + body (RtmpOutput | WhipOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RtmpOutput | WhipOutput, +) -> ApiError | EmptyResponse | None: + """Register an output + + Args: + composition_id (str): + output_id (str): + body (RtmpOutput | WhipOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/outputs/register_template_output.py b/fishjam/_composition_client/api/outputs/register_template_output.py new file mode 100644 index 0000000..0ed92f1 --- /dev/null +++ b/fishjam/_composition_client/api/outputs/register_template_output.py @@ -0,0 +1,214 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.register_template_output_body import RegisterTemplateOutputBody +from ...types import Response + + +def _get_kwargs( + composition_id: str, + output_id: str, + *, + body: RegisterTemplateOutputBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/output/{output_id}/template".format( + composition_id=quote(str(composition_id), safe=""), + output_id=quote(str(output_id), safe=""), + ), + } + + _kwargs["files"] = body.to_multipart() + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if response.status_code == 503: + response_503 = ApiError.from_dict(response.json()) + + return response_503 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RegisterTemplateOutputBody, +) -> Response[ApiError | EmptyResponse]: + """Register a templated output + + Args: + composition_id (str): + output_id (str): + body (RegisterTemplateOutputBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RegisterTemplateOutputBody, +) -> ApiError | EmptyResponse | None: + """Register a templated output + + Args: + composition_id (str): + output_id (str): + body (RegisterTemplateOutputBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RegisterTemplateOutputBody, +) -> Response[ApiError | EmptyResponse]: + """Register a templated output + + Args: + composition_id (str): + output_id (str): + body (RegisterTemplateOutputBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: RegisterTemplateOutputBody, +) -> ApiError | EmptyResponse | None: + """Register a templated output + + Args: + composition_id (str): + output_id (str): + body (RegisterTemplateOutputBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/outputs/request_keyframe.py b/fishjam/_composition_client/api/outputs/request_keyframe.py new file mode 100644 index 0000000..62ce71b --- /dev/null +++ b/fishjam/_composition_client/api/outputs/request_keyframe.py @@ -0,0 +1,189 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...types import Response + + +def _get_kwargs( + composition_id: str, + output_id: str, +) -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/output/{output_id}/request_keyframe".format( + composition_id=quote(str(composition_id), safe=""), + output_id=quote(str(output_id), safe=""), + ), + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, +) -> Response[ApiError | EmptyResponse]: + """Request a keyframe + + Args: + composition_id (str): + output_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, +) -> ApiError | EmptyResponse | None: + """Request a keyframe + + Args: + composition_id (str): + output_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, +) -> Response[ApiError | EmptyResponse]: + """Request a keyframe + + Args: + composition_id (str): + output_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, +) -> ApiError | EmptyResponse | None: + """Request a keyframe + + Args: + composition_id (str): + output_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + ) + ).parsed diff --git a/fishjam/_composition_client/api/outputs/unregister_output.py b/fishjam/_composition_client/api/outputs/unregister_output.py new file mode 100644 index 0000000..bc55b01 --- /dev/null +++ b/fishjam/_composition_client/api/outputs/unregister_output.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.unregister_output import UnregisterOutput +from ...types import Response + + +def _get_kwargs( + composition_id: str, + output_id: str, + *, + body: UnregisterOutput, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/output/{output_id}/unregister".format( + composition_id=quote(str(composition_id), safe=""), + output_id=quote(str(output_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UnregisterOutput, +) -> Response[ApiError | EmptyResponse]: + """Unregister an output + + Args: + composition_id (str): + output_id (str): + body (UnregisterOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UnregisterOutput, +) -> ApiError | EmptyResponse | None: + """Unregister an output + + Args: + composition_id (str): + output_id (str): + body (UnregisterOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UnregisterOutput, +) -> Response[ApiError | EmptyResponse]: + """Unregister an output + + Args: + composition_id (str): + output_id (str): + body (UnregisterOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UnregisterOutput, +) -> ApiError | EmptyResponse | None: + """Unregister an output + + Args: + composition_id (str): + output_id (str): + body (UnregisterOutput): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/outputs/update_output.py b/fishjam/_composition_client/api/outputs/update_output.py new file mode 100644 index 0000000..df65d06 --- /dev/null +++ b/fishjam/_composition_client/api/outputs/update_output.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.update_output_request import UpdateOutputRequest +from ...types import Response + + +def _get_kwargs( + composition_id: str, + output_id: str, + *, + body: UpdateOutputRequest, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/output/{output_id}/update".format( + composition_id=quote(str(composition_id), safe=""), + output_id=quote(str(output_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UpdateOutputRequest, +) -> Response[ApiError | EmptyResponse]: + """Update an output's scene + + Args: + composition_id (str): + output_id (str): + body (UpdateOutputRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UpdateOutputRequest, +) -> ApiError | EmptyResponse | None: + """Update an output's scene + + Args: + composition_id (str): + output_id (str): + body (UpdateOutputRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UpdateOutputRequest, +) -> Response[ApiError | EmptyResponse]: + """Update an output's scene + + Args: + composition_id (str): + output_id (str): + body (UpdateOutputRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + output_id=output_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + output_id: str, + *, + client: AuthenticatedClient, + body: UpdateOutputRequest, +) -> ApiError | EmptyResponse | None: + """Update an output's scene + + Args: + composition_id (str): + output_id (str): + body (UpdateOutputRequest): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + output_id=output_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/renderers/__init__.py b/fishjam/_composition_client/api/renderers/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_composition_client/api/renderers/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_composition_client/api/renderers/register_font.py b/fishjam/_composition_client/api/renderers/register_font.py new file mode 100644 index 0000000..235992d --- /dev/null +++ b/fishjam/_composition_client/api/renderers/register_font.py @@ -0,0 +1,195 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.register_font_body import RegisterFontBody +from ...types import Response + + +def _get_kwargs( + composition_id: str, + *, + body: RegisterFontBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/font/register".format( + composition_id=quote(str(composition_id), safe=""), + ), + } + + _kwargs["files"] = body.to_multipart() + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + *, + client: AuthenticatedClient, + body: RegisterFontBody, +) -> Response[ApiError | EmptyResponse]: + """Register a font + + Args: + composition_id (str): + body (RegisterFontBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + *, + client: AuthenticatedClient, + body: RegisterFontBody, +) -> ApiError | EmptyResponse | None: + """Register a font + + Args: + composition_id (str): + body (RegisterFontBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + *, + client: AuthenticatedClient, + body: RegisterFontBody, +) -> Response[ApiError | EmptyResponse]: + """Register a font + + Args: + composition_id (str): + body (RegisterFontBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + *, + client: AuthenticatedClient, + body: RegisterFontBody, +) -> ApiError | EmptyResponse | None: + """Register a font + + Args: + composition_id (str): + body (RegisterFontBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/renderers/register_image.py b/fishjam/_composition_client/api/renderers/register_image.py new file mode 100644 index 0000000..0924e26 --- /dev/null +++ b/fishjam/_composition_client/api/renderers/register_image.py @@ -0,0 +1,229 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.image_spec_auto import ImageSpecAuto +from ...models.image_spec_gif import ImageSpecGif +from ...models.image_spec_jpeg import ImageSpecJpeg +from ...models.image_spec_png import ImageSpecPng +from ...models.image_spec_svg import ImageSpecSvg +from ...types import Response + + +def _get_kwargs( + composition_id: str, + image_id: str, + *, + body: ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/image/{image_id}/register".format( + composition_id=quote(str(composition_id), safe=""), + image_id=quote(str(image_id), safe=""), + ), + } + + if isinstance(body, ImageSpecPng): + _kwargs["json"] = body.to_dict() + elif isinstance(body, ImageSpecJpeg): + _kwargs["json"] = body.to_dict() + elif isinstance(body, ImageSpecSvg): + _kwargs["json"] = body.to_dict() + elif isinstance(body, ImageSpecGif): + _kwargs["json"] = body.to_dict() + else: + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg, +) -> Response[ApiError | EmptyResponse]: + """Register an image + + Args: + composition_id (str): + image_id (str): + body (ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + image_id=image_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg, +) -> ApiError | EmptyResponse | None: + """Register an image + + Args: + composition_id (str): + image_id (str): + body (ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + image_id=image_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg, +) -> Response[ApiError | EmptyResponse]: + """Register an image + + Args: + composition_id (str): + image_id (str): + body (ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + image_id=image_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg, +) -> ApiError | EmptyResponse | None: + """Register an image + + Args: + composition_id (str): + image_id (str): + body (ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + image_id=image_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/api/renderers/unregister_image.py b/fishjam/_composition_client/api/renderers/unregister_image.py new file mode 100644 index 0000000..f367eff --- /dev/null +++ b/fishjam/_composition_client/api/renderers/unregister_image.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.api_error import ApiError +from ...models.empty_response import EmptyResponse +from ...models.unregister_renderer import UnregisterRenderer +from ...types import Response + + +def _get_kwargs( + composition_id: str, + image_id: str, + *, + body: UnregisterRenderer, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/composition/{composition_id}/image/{image_id}/unregister".format( + composition_id=quote(str(composition_id), safe=""), + image_id=quote(str(image_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ApiError | EmptyResponse | None: + if response.status_code == 200: + response_200 = EmptyResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ApiError.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ApiError.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ApiError.from_dict(response.json()) + + return response_404 + + if response.status_code == 422: + response_422 = ApiError.from_dict(response.json()) + + return response_422 + + if response.status_code == 500: + response_500 = ApiError.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ApiError | EmptyResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: UnregisterRenderer, +) -> Response[ApiError | EmptyResponse]: + """Unregister an image + + Args: + composition_id (str): + image_id (str): + body (UnregisterRenderer): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + image_id=image_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: UnregisterRenderer, +) -> ApiError | EmptyResponse | None: + """Unregister an image + + Args: + composition_id (str): + image_id (str): + body (UnregisterRenderer): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return sync_detailed( + composition_id=composition_id, + image_id=image_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: UnregisterRenderer, +) -> Response[ApiError | EmptyResponse]: + """Unregister an image + + Args: + composition_id (str): + image_id (str): + body (UnregisterRenderer): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ApiError | EmptyResponse] + """ + + kwargs = _get_kwargs( + composition_id=composition_id, + image_id=image_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + composition_id: str, + image_id: str, + *, + client: AuthenticatedClient, + body: UnregisterRenderer, +) -> ApiError | EmptyResponse | None: + """Unregister an image + + Args: + composition_id (str): + image_id (str): + body (UnregisterRenderer): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ApiError | EmptyResponse + """ + + return ( + await asyncio_detailed( + composition_id=composition_id, + image_id=image_id, + client=client, + body=body, + ) + ).parsed diff --git a/fishjam/_composition_client/client.py b/fishjam/_composition_client/client.py new file mode 100644 index 0000000..0ab1589 --- /dev/null +++ b/fishjam/_composition_client/client.py @@ -0,0 +1,282 @@ +import ssl +from typing import Any + +import httpx +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field( + default=True, kw_only=True, alias="verify_ssl" + ) + _follow_redirects: bool = field( + default=False, kw_only=True, alias="follow_redirects" + ) + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "Client": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "Client": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "Client": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field( + default=True, kw_only=True, alias="verify_ssl" + ) + _follow_redirects: bool = field( + default=False, kw_only=True, alias="follow_redirects" + ) + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = ( + f"{self.prefix} {self.token}" if self.prefix else self.token + ) + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "AuthenticatedClient": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client( + self, async_client: httpx.AsyncClient + ) -> "AuthenticatedClient": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = ( + f"{self.prefix} {self.token}" if self.prefix else self.token + ) + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "AuthenticatedClient": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/fishjam/_composition_client/errors.py b/fishjam/_composition_client/errors.py new file mode 100644 index 0000000..5f92e76 --- /dev/null +++ b/fishjam/_composition_client/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/fishjam/_composition_client/models/__init__.py b/fishjam/_composition_client/models/__init__.py new file mode 100644 index 0000000..1a5e00a --- /dev/null +++ b/fishjam/_composition_client/models/__init__.py @@ -0,0 +1,177 @@ +"""Contains all the data models used in inputs/outputs""" + +from .api_error import ApiError +from .audio_channels import AudioChannels +from .audio_mixing_strategy import AudioMixingStrategy +from .audio_scene import AudioScene +from .audio_scene_input import AudioSceneInput +from .average_and_max_bitrate import AverageAndMaxBitrate +from .box_shadow import BoxShadow +from .composition_created_response import CompositionCreatedResponse +from .create_composition_request import CreateCompositionRequest +from .easing_function_bounce import EasingFunctionBounce +from .easing_function_bounce_function_name import EasingFunctionBounceFunctionName +from .easing_function_cubic_bezier import EasingFunctionCubicBezier +from .easing_function_cubic_bezier_function_name import ( + EasingFunctionCubicBezierFunctionName, +) +from .easing_function_linear import EasingFunctionLinear +from .easing_function_linear_function_name import EasingFunctionLinearFunctionName +from .empty_response import EmptyResponse +from .font_upload import FontUpload +from .h264_encoder_preset import H264EncoderPreset +from .horizontal_align import HorizontalAlign +from .image import Image +from .image_spec_auto import ImageSpecAuto +from .image_spec_auto_asset_type import ImageSpecAutoAssetType +from .image_spec_gif import ImageSpecGif +from .image_spec_gif_asset_type import ImageSpecGifAssetType +from .image_spec_jpeg import ImageSpecJpeg +from .image_spec_jpeg_asset_type import ImageSpecJpegAssetType +from .image_spec_png import ImageSpecPng +from .image_spec_png_asset_type import ImageSpecPngAssetType +from .image_spec_svg import ImageSpecSvg +from .image_spec_svg_asset_type import ImageSpecSvgAssetType +from .image_type import ImageType +from .input_stream import InputStream +from .input_stream_type import InputStreamType +from .interpolation import Interpolation +from .mp_4_input import Mp4Input +from .mp_4_input_type import Mp4InputType +from .opus_encoder_preset import OpusEncoderPreset +from .output_end_condition import OutputEndCondition +from .output_rtmp_client_audio_options import OutputRtmpClientAudioOptions +from .output_rtmp_client_video_options import OutputRtmpClientVideoOptions +from .output_whip_audio_options import OutputWhipAudioOptions +from .output_whip_video_options import OutputWhipVideoOptions +from .overflow import Overflow +from .pixel_format import PixelFormat +from .register_font_body import RegisterFontBody +from .register_input_response import RegisterInputResponse +from .register_template_output import RegisterTemplateOutput +from .register_template_output_body import RegisterTemplateOutputBody +from .rescale_mode import RescaleMode +from .rescaler import Rescaler +from .rescaler_type import RescalerType +from .resolution import Resolution +from .rtmp_input import RtmpInput +from .rtmp_input_type import RtmpInputType +from .rtmp_output import RtmpOutput +from .rtmp_output_type import RtmpOutputType +from .send_composition_event_body import SendCompositionEventBody +from .text import Text +from .text_style import TextStyle +from .text_type import TextType +from .text_weight import TextWeight +from .text_wrap_mode import TextWrapMode +from .tiles import Tiles +from .tiles_type import TilesType +from .transition import Transition +from .transport_protocol import TransportProtocol +from .unregister_input import UnregisterInput +from .unregister_output import UnregisterOutput +from .unregister_renderer import UnregisterRenderer +from .update_output_request import UpdateOutputRequest +from .vertical_align import VerticalAlign +from .video_scene import VideoScene +from .view import View +from .view_direction import ViewDirection +from .view_type import ViewType +from .whep_input import WhepInput +from .whep_input_type import WhepInputType +from .whip_audio_encoder_options_any import WhipAudioEncoderOptionsAny +from .whip_audio_encoder_options_any_type import WhipAudioEncoderOptionsAnyType +from .whip_audio_encoder_options_opus import WhipAudioEncoderOptionsOpus +from .whip_audio_encoder_options_opus_type import WhipAudioEncoderOptionsOpusType +from .whip_input import WhipInput +from .whip_input_type import WhipInputType +from .whip_output import WhipOutput +from .whip_output_type import WhipOutputType + +__all__ = ( + "ApiError", + "AudioChannels", + "AudioMixingStrategy", + "AudioScene", + "AudioSceneInput", + "AverageAndMaxBitrate", + "BoxShadow", + "CompositionCreatedResponse", + "CreateCompositionRequest", + "EasingFunctionBounce", + "EasingFunctionBounceFunctionName", + "EasingFunctionCubicBezier", + "EasingFunctionCubicBezierFunctionName", + "EasingFunctionLinear", + "EasingFunctionLinearFunctionName", + "EmptyResponse", + "FontUpload", + "H264EncoderPreset", + "HorizontalAlign", + "Image", + "ImageSpecAuto", + "ImageSpecAutoAssetType", + "ImageSpecGif", + "ImageSpecGifAssetType", + "ImageSpecJpeg", + "ImageSpecJpegAssetType", + "ImageSpecPng", + "ImageSpecPngAssetType", + "ImageSpecSvg", + "ImageSpecSvgAssetType", + "ImageType", + "InputStream", + "InputStreamType", + "Interpolation", + "Mp4Input", + "Mp4InputType", + "OpusEncoderPreset", + "OutputEndCondition", + "OutputRtmpClientAudioOptions", + "OutputRtmpClientVideoOptions", + "OutputWhipAudioOptions", + "OutputWhipVideoOptions", + "Overflow", + "PixelFormat", + "RegisterFontBody", + "RegisterInputResponse", + "RegisterTemplateOutput", + "RegisterTemplateOutputBody", + "RescaleMode", + "Rescaler", + "RescalerType", + "Resolution", + "RtmpInput", + "RtmpInputType", + "RtmpOutput", + "RtmpOutputType", + "SendCompositionEventBody", + "Text", + "TextStyle", + "TextType", + "TextWeight", + "TextWrapMode", + "Tiles", + "TilesType", + "Transition", + "TransportProtocol", + "UnregisterInput", + "UnregisterOutput", + "UnregisterRenderer", + "UpdateOutputRequest", + "VerticalAlign", + "VideoScene", + "View", + "ViewDirection", + "ViewType", + "WhepInput", + "WhepInputType", + "WhipAudioEncoderOptionsAny", + "WhipAudioEncoderOptionsAnyType", + "WhipAudioEncoderOptionsOpus", + "WhipAudioEncoderOptionsOpusType", + "WhipInput", + "WhipInputType", + "WhipOutput", + "WhipOutputType", +) diff --git a/fishjam/_composition_client/models/api_error.py b/fishjam/_composition_client/models/api_error.py new file mode 100644 index 0000000..7286535 --- /dev/null +++ b/fishjam/_composition_client/models/api_error.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ApiError") + + +@_attrs_define +class ApiError: + """ + Attributes: + message (str): + """ + + message: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + message = self.message + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "message": message, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + message = d.pop("message") + + api_error = cls( + message=message, + ) + + api_error.additional_properties = d + return api_error + + @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/_composition_client/models/audio_channels.py b/fishjam/_composition_client/models/audio_channels.py new file mode 100644 index 0000000..6b74e5d --- /dev/null +++ b/fishjam/_composition_client/models/audio_channels.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class AudioChannels(str, Enum): + """None""" + + MONO = "mono" + STEREO = "stereo" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/audio_mixing_strategy.py b/fishjam/_composition_client/models/audio_mixing_strategy.py new file mode 100644 index 0000000..49c1e9a --- /dev/null +++ b/fishjam/_composition_client/models/audio_mixing_strategy.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class AudioMixingStrategy(str, Enum): + """None""" + + SUM_CLIP = "sum_clip" + SUM_SCALE = "sum_scale" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/audio_scene.py b/fishjam/_composition_client/models/audio_scene.py new file mode 100644 index 0000000..b55c23c --- /dev/null +++ b/fishjam/_composition_client/models/audio_scene.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.audio_scene_input import AudioSceneInput + + +T = TypeVar("T", bound="AudioScene") + + +@_attrs_define +class AudioScene: + """ + Attributes: + inputs (list[AudioSceneInput]): + """ + + inputs: list[AudioSceneInput] + + def to_dict(self) -> dict[str, Any]: + inputs = [] + for inputs_item_data in self.inputs: + inputs_item = inputs_item_data.to_dict() + inputs.append(inputs_item) + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "inputs": inputs, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.audio_scene_input import AudioSceneInput + + d = dict(src_dict) + inputs = [] + _inputs = d.pop("inputs") + for inputs_item_data in _inputs: + inputs_item = AudioSceneInput.from_dict(inputs_item_data) + + inputs.append(inputs_item) + + audio_scene = cls( + inputs=inputs, + ) + + return audio_scene diff --git a/fishjam/_composition_client/models/audio_scene_input.py b/fishjam/_composition_client/models/audio_scene_input.py new file mode 100644 index 0000000..1259037 --- /dev/null +++ b/fishjam/_composition_client/models/audio_scene_input.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AudioSceneInput") + + +@_attrs_define +class AudioSceneInput: + """ + Attributes: + input_id (str): + volume (float | None | Unset): (**default=`1.0`**) float in `[0, 2]` range representing input volume + """ + + input_id: str + volume: float | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + input_id = self.input_id + + volume: float | None | Unset + if isinstance(self.volume, Unset): + volume = UNSET + else: + volume = self.volume + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "input_id": input_id, + }) + if volume is not UNSET: + field_dict["volume"] = volume + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + input_id = d.pop("input_id") + + def _parse_volume(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + volume = _parse_volume(d.pop("volume", UNSET)) + + audio_scene_input = cls( + input_id=input_id, + volume=volume, + ) + + return audio_scene_input diff --git a/fishjam/_composition_client/models/average_and_max_bitrate.py b/fishjam/_composition_client/models/average_and_max_bitrate.py new file mode 100644 index 0000000..64999a2 --- /dev/null +++ b/fishjam/_composition_client/models/average_and_max_bitrate.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="AverageAndMaxBitrate") + + +@_attrs_define +class AverageAndMaxBitrate: + """ + Attributes: + average_bitrate (int): Average bitrate measured in bits/second. Encoder will try to keep the bitrate around the + provided average, + but may temporarily increase it to the provided max bitrate. + max_bitrate (int): Max bitrate measured in bits/second. + """ + + average_bitrate: int + max_bitrate: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + average_bitrate = self.average_bitrate + + max_bitrate = self.max_bitrate + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "average_bitrate": average_bitrate, + "max_bitrate": max_bitrate, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + average_bitrate = d.pop("average_bitrate") + + max_bitrate = d.pop("max_bitrate") + + average_and_max_bitrate = cls( + average_bitrate=average_bitrate, + max_bitrate=max_bitrate, + ) + + average_and_max_bitrate.additional_properties = d + return average_and_max_bitrate + + @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/_composition_client/models/box_shadow.py b/fishjam/_composition_client/models/box_shadow.py new file mode 100644 index 0000000..f6c682c --- /dev/null +++ b/fishjam/_composition_client/models/box_shadow.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BoxShadow") + + +@_attrs_define +class BoxShadow: + """ + Attributes: + offset_x (float | None | Unset): + offset_y (float | None | Unset): + color (None | str | Unset): + blur_radius (float | None | Unset): + """ + + offset_x: float | None | Unset = UNSET + offset_y: float | None | Unset = UNSET + color: None | str | Unset = UNSET + blur_radius: float | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + offset_x: float | None | Unset + if isinstance(self.offset_x, Unset): + offset_x = UNSET + else: + offset_x = self.offset_x + + offset_y: float | None | Unset + if isinstance(self.offset_y, Unset): + offset_y = UNSET + else: + offset_y = self.offset_y + + color: None | str | Unset + if isinstance(self.color, Unset): + color = UNSET + else: + color = self.color + + blur_radius: float | None | Unset + if isinstance(self.blur_radius, Unset): + blur_radius = UNSET + else: + blur_radius = self.blur_radius + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if offset_x is not UNSET: + field_dict["offset_x"] = offset_x + if offset_y is not UNSET: + field_dict["offset_y"] = offset_y + if color is not UNSET: + field_dict["color"] = color + if blur_radius is not UNSET: + field_dict["blur_radius"] = blur_radius + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_offset_x(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + offset_x = _parse_offset_x(d.pop("offset_x", UNSET)) + + def _parse_offset_y(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + offset_y = _parse_offset_y(d.pop("offset_y", UNSET)) + + def _parse_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + color = _parse_color(d.pop("color", UNSET)) + + def _parse_blur_radius(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + blur_radius = _parse_blur_radius(d.pop("blur_radius", UNSET)) + + box_shadow = cls( + offset_x=offset_x, + offset_y=offset_y, + color=color, + blur_radius=blur_radius, + ) + + return box_shadow diff --git a/fishjam/_composition_client/models/composition_created_response.py b/fishjam/_composition_client/models/composition_created_response.py new file mode 100644 index 0000000..a6dc274 --- /dev/null +++ b/fishjam/_composition_client/models/composition_created_response.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="CompositionCreatedResponse") + + +@_attrs_define +class CompositionCreatedResponse: + """ + Attributes: + composition_id (str): + api_url (str): + """ + + composition_id: str + api_url: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + composition_id = self.composition_id + + api_url = self.api_url + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "composition_id": composition_id, + "api_url": api_url, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + composition_id = d.pop("composition_id") + + api_url = d.pop("api_url") + + composition_created_response = cls( + composition_id=composition_id, + api_url=api_url, + ) + + composition_created_response.additional_properties = d + return composition_created_response + + @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/_composition_client/models/create_composition_request.py b/fishjam/_composition_client/models/create_composition_request.py new file mode 100644 index 0000000..d737a04 --- /dev/null +++ b/fishjam/_composition_client/models/create_composition_request.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CreateCompositionRequest") + + +@_attrs_define +class CreateCompositionRequest: + """ + Attributes: + autostart (bool | Unset): If true, outputs will immediately start producing audio and video. + If false, call `POST /api/composition/{composition_id}/start` to start the composition. Default: True. + cleanup_without_inputs (bool | Unset): If true (default), the composition will be cleaned up after 5 minutes + when all **inputs** + have zero bitrate, regardless of output bitrate. This prevents circular liveness when + composition output is sent to a stream. + If false, cleanup only triggers when both inputs and outputs have zero bitrate. Default: True. + """ + + autostart: bool | Unset = True + cleanup_without_inputs: bool | Unset = True + + def to_dict(self) -> dict[str, Any]: + autostart = self.autostart + + cleanup_without_inputs = self.cleanup_without_inputs + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if autostart is not UNSET: + field_dict["autostart"] = autostart + if cleanup_without_inputs is not UNSET: + field_dict["cleanup_without_inputs"] = cleanup_without_inputs + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + autostart = d.pop("autostart", UNSET) + + cleanup_without_inputs = d.pop("cleanup_without_inputs", UNSET) + + create_composition_request = cls( + autostart=autostart, + cleanup_without_inputs=cleanup_without_inputs, + ) + + return create_composition_request diff --git a/fishjam/_composition_client/models/easing_function_bounce.py b/fishjam/_composition_client/models/easing_function_bounce.py new file mode 100644 index 0000000..b5125f6 --- /dev/null +++ b/fishjam/_composition_client/models/easing_function_bounce.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.easing_function_bounce_function_name import ( + EasingFunctionBounceFunctionName, +) + +T = TypeVar("T", bound="EasingFunctionBounce") + + +@_attrs_define +class EasingFunctionBounce: + """ + Attributes: + function_name (EasingFunctionBounceFunctionName): + """ + + function_name: EasingFunctionBounceFunctionName + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + function_name = self.function_name.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "function_name": function_name, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + function_name = EasingFunctionBounceFunctionName(d.pop("function_name")) + + easing_function_bounce = cls( + function_name=function_name, + ) + + easing_function_bounce.additional_properties = d + return easing_function_bounce + + @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/_composition_client/models/easing_function_bounce_function_name.py b/fishjam/_composition_client/models/easing_function_bounce_function_name.py new file mode 100644 index 0000000..ccbc9f1 --- /dev/null +++ b/fishjam/_composition_client/models/easing_function_bounce_function_name.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class EasingFunctionBounceFunctionName(str, Enum): + """None""" + + BOUNCE = "bounce" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/easing_function_cubic_bezier.py b/fishjam/_composition_client/models/easing_function_cubic_bezier.py new file mode 100644 index 0000000..7a96ee8 --- /dev/null +++ b/fishjam/_composition_client/models/easing_function_cubic_bezier.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.easing_function_cubic_bezier_function_name import ( + EasingFunctionCubicBezierFunctionName, +) + +T = TypeVar("T", bound="EasingFunctionCubicBezier") + + +@_attrs_define +class EasingFunctionCubicBezier: + """ + Attributes: + points (list[float]): + function_name (EasingFunctionCubicBezierFunctionName): + """ + + points: list[float] + function_name: EasingFunctionCubicBezierFunctionName + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + points = self.points + + function_name = self.function_name.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "points": points, + "function_name": function_name, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + points = cast(list[float], d.pop("points")) + + function_name = EasingFunctionCubicBezierFunctionName(d.pop("function_name")) + + easing_function_cubic_bezier = cls( + points=points, + function_name=function_name, + ) + + easing_function_cubic_bezier.additional_properties = d + return easing_function_cubic_bezier + + @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/_composition_client/models/easing_function_cubic_bezier_function_name.py b/fishjam/_composition_client/models/easing_function_cubic_bezier_function_name.py new file mode 100644 index 0000000..4b63e27 --- /dev/null +++ b/fishjam/_composition_client/models/easing_function_cubic_bezier_function_name.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class EasingFunctionCubicBezierFunctionName(str, Enum): + """None""" + + CUBIC_BEZIER = "cubic_bezier" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/easing_function_linear.py b/fishjam/_composition_client/models/easing_function_linear.py new file mode 100644 index 0000000..1112a4a --- /dev/null +++ b/fishjam/_composition_client/models/easing_function_linear.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.easing_function_linear_function_name import ( + EasingFunctionLinearFunctionName, +) + +T = TypeVar("T", bound="EasingFunctionLinear") + + +@_attrs_define +class EasingFunctionLinear: + """ + Attributes: + function_name (EasingFunctionLinearFunctionName): + """ + + function_name: EasingFunctionLinearFunctionName + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + function_name = self.function_name.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "function_name": function_name, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + function_name = EasingFunctionLinearFunctionName(d.pop("function_name")) + + easing_function_linear = cls( + function_name=function_name, + ) + + easing_function_linear.additional_properties = d + return easing_function_linear + + @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/_composition_client/models/easing_function_linear_function_name.py b/fishjam/_composition_client/models/easing_function_linear_function_name.py new file mode 100644 index 0000000..7f64cb3 --- /dev/null +++ b/fishjam/_composition_client/models/easing_function_linear_function_name.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class EasingFunctionLinearFunctionName(str, Enum): + """None""" + + LINEAR = "linear" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/empty_response.py b/fishjam/_composition_client/models/empty_response.py new file mode 100644 index 0000000..1d1f80c --- /dev/null +++ b/fishjam/_composition_client/models/empty_response.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="EmptyResponse") + + +@_attrs_define +class EmptyResponse: + """ """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + empty_response = cls() + + empty_response.additional_properties = d + return empty_response + + @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/_composition_client/models/font_upload.py b/fishjam/_composition_client/models/font_upload.py new file mode 100644 index 0000000..cc7d6f3 --- /dev/null +++ b/fishjam/_composition_client/models/font_upload.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from io import BytesIO +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import File + +T = TypeVar("T", bound="FontUpload") + + +@_attrs_define +class FontUpload: + """ + Attributes: + font (File): + """ + + font: File + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + font = self.font.to_tuple() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "font": font, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + font = File(payload=BytesIO(d.pop("font"))) + + font_upload = cls( + font=font, + ) + + font_upload.additional_properties = d + return font_upload + + @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/_composition_client/models/h264_encoder_preset.py b/fishjam/_composition_client/models/h264_encoder_preset.py new file mode 100644 index 0000000..2ec1e58 --- /dev/null +++ b/fishjam/_composition_client/models/h264_encoder_preset.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class H264EncoderPreset(str, Enum): + """None""" + + FAST = "fast" + FASTER = "faster" + MEDIUM = "medium" + PLACEBO = "placebo" + SLOW = "slow" + SLOWER = "slower" + SUPERFAST = "superfast" + ULTRAFAST = "ultrafast" + VERYFAST = "veryfast" + VERYSLOW = "veryslow" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/horizontal_align.py b/fishjam/_composition_client/models/horizontal_align.py new file mode 100644 index 0000000..4849499 --- /dev/null +++ b/fishjam/_composition_client/models/horizontal_align.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class HorizontalAlign(str, Enum): + """None""" + + CENTER = "center" + JUSTIFIED = "justified" + LEFT = "left" + RIGHT = "right" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/image.py b/fishjam/_composition_client/models/image.py new file mode 100644 index 0000000..01804d9 --- /dev/null +++ b/fishjam/_composition_client/models/image.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.image_type import ImageType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Image") + + +@_attrs_define +class Image: + """ + Attributes: + image_id (str): + type_ (ImageType): + id (None | str | Unset): + width (float | None | Unset): Width of the image in pixels. + If `height` is not explicitly provided, the image will automatically adjust its height to maintain its original + aspect ratio relative to the width. + height (float | None | Unset): Height of the image in pixels. + If `width` is not explicitly provided, the image will automatically adjust its width to maintain its original + aspect ratio relative to the height. + """ + + image_id: str + type_: ImageType + id: None | str | Unset = UNSET + width: float | None | Unset = UNSET + height: float | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + image_id = self.image_id + + type_ = self.type_.value + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + width: float | None | Unset + if isinstance(self.width, Unset): + width = UNSET + else: + width = self.width + + height: float | None | Unset + if isinstance(self.height, Unset): + height = UNSET + else: + height = self.height + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "image_id": image_id, + "type": type_, + }) + if id is not UNSET: + field_dict["id"] = id + if width is not UNSET: + field_dict["width"] = width + if height is not UNSET: + field_dict["height"] = height + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + image_id = d.pop("image_id") + + type_ = ImageType(d.pop("type")) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + width = _parse_width(d.pop("width", UNSET)) + + def _parse_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + height = _parse_height(d.pop("height", UNSET)) + + image = cls( + image_id=image_id, + type_=type_, + id=id, + width=width, + height=height, + ) + + return image diff --git a/fishjam/_composition_client/models/image_spec_auto.py b/fishjam/_composition_client/models/image_spec_auto.py new file mode 100644 index 0000000..bc91d3c --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_auto.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.image_spec_auto_asset_type import ImageSpecAutoAssetType + +T = TypeVar("T", bound="ImageSpecAuto") + + +@_attrs_define +class ImageSpecAuto: + """ + Attributes: + url (str): + asset_type (ImageSpecAutoAssetType): + """ + + url: str + asset_type: ImageSpecAutoAssetType + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + asset_type = self.asset_type.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "url": url, + "asset_type": asset_type, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + url = d.pop("url") + + asset_type = ImageSpecAutoAssetType(d.pop("asset_type")) + + image_spec_auto = cls( + url=url, + asset_type=asset_type, + ) + + image_spec_auto.additional_properties = d + return image_spec_auto + + @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/_composition_client/models/image_spec_auto_asset_type.py b/fishjam/_composition_client/models/image_spec_auto_asset_type.py new file mode 100644 index 0000000..f4b4677 --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_auto_asset_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ImageSpecAutoAssetType(str, Enum): + """None""" + + AUTO = "auto" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/image_spec_gif.py b/fishjam/_composition_client/models/image_spec_gif.py new file mode 100644 index 0000000..36ee2d7 --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_gif.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.image_spec_gif_asset_type import ImageSpecGifAssetType + +T = TypeVar("T", bound="ImageSpecGif") + + +@_attrs_define +class ImageSpecGif: + """ + Attributes: + url (str): + asset_type (ImageSpecGifAssetType): + """ + + url: str + asset_type: ImageSpecGifAssetType + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + asset_type = self.asset_type.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "url": url, + "asset_type": asset_type, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + url = d.pop("url") + + asset_type = ImageSpecGifAssetType(d.pop("asset_type")) + + image_spec_gif = cls( + url=url, + asset_type=asset_type, + ) + + image_spec_gif.additional_properties = d + return image_spec_gif + + @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/_composition_client/models/image_spec_gif_asset_type.py b/fishjam/_composition_client/models/image_spec_gif_asset_type.py new file mode 100644 index 0000000..940c3bc --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_gif_asset_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ImageSpecGifAssetType(str, Enum): + """None""" + + GIF = "gif" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/image_spec_jpeg.py b/fishjam/_composition_client/models/image_spec_jpeg.py new file mode 100644 index 0000000..ebbffab --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_jpeg.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.image_spec_jpeg_asset_type import ImageSpecJpegAssetType + +T = TypeVar("T", bound="ImageSpecJpeg") + + +@_attrs_define +class ImageSpecJpeg: + """ + Attributes: + url (str): + asset_type (ImageSpecJpegAssetType): + """ + + url: str + asset_type: ImageSpecJpegAssetType + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + asset_type = self.asset_type.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "url": url, + "asset_type": asset_type, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + url = d.pop("url") + + asset_type = ImageSpecJpegAssetType(d.pop("asset_type")) + + image_spec_jpeg = cls( + url=url, + asset_type=asset_type, + ) + + image_spec_jpeg.additional_properties = d + return image_spec_jpeg + + @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/_composition_client/models/image_spec_jpeg_asset_type.py b/fishjam/_composition_client/models/image_spec_jpeg_asset_type.py new file mode 100644 index 0000000..560b00a --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_jpeg_asset_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ImageSpecJpegAssetType(str, Enum): + """None""" + + JPEG = "jpeg" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/image_spec_png.py b/fishjam/_composition_client/models/image_spec_png.py new file mode 100644 index 0000000..204a207 --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_png.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.image_spec_png_asset_type import ImageSpecPngAssetType + +T = TypeVar("T", bound="ImageSpecPng") + + +@_attrs_define +class ImageSpecPng: + """ + Attributes: + url (str): + asset_type (ImageSpecPngAssetType): + """ + + url: str + asset_type: ImageSpecPngAssetType + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + url = self.url + + asset_type = self.asset_type.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "url": url, + "asset_type": asset_type, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + url = d.pop("url") + + asset_type = ImageSpecPngAssetType(d.pop("asset_type")) + + image_spec_png = cls( + url=url, + asset_type=asset_type, + ) + + image_spec_png.additional_properties = d + return image_spec_png + + @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/_composition_client/models/image_spec_png_asset_type.py b/fishjam/_composition_client/models/image_spec_png_asset_type.py new file mode 100644 index 0000000..965a602 --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_png_asset_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ImageSpecPngAssetType(str, Enum): + """None""" + + PNG = "png" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/image_spec_svg.py b/fishjam/_composition_client/models/image_spec_svg.py new file mode 100644 index 0000000..4f541e2 --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_svg.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.image_spec_svg_asset_type import ImageSpecSvgAssetType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.resolution import Resolution + + +T = TypeVar("T", bound="ImageSpecSvg") + + +@_attrs_define +class ImageSpecSvg: + """ + Attributes: + url (str): + asset_type (ImageSpecSvgAssetType): + resolution (None | Resolution | Unset): + """ + + url: str + asset_type: ImageSpecSvgAssetType + resolution: None | Resolution | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.resolution import Resolution + + url = self.url + + asset_type = self.asset_type.value + + resolution: dict[str, Any] | None | Unset + if isinstance(self.resolution, Unset): + resolution = UNSET + elif isinstance(self.resolution, Resolution): + resolution = self.resolution.to_dict() + else: + resolution = self.resolution + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "url": url, + "asset_type": asset_type, + }) + if resolution is not UNSET: + field_dict["resolution"] = resolution + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.resolution import Resolution + + d = dict(src_dict) + url = d.pop("url") + + asset_type = ImageSpecSvgAssetType(d.pop("asset_type")) + + def _parse_resolution(data: object) -> None | Resolution | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + resolution_type_1 = Resolution.from_dict(data) + + return resolution_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Resolution | Unset, data) + + resolution = _parse_resolution(d.pop("resolution", UNSET)) + + image_spec_svg = cls( + url=url, + asset_type=asset_type, + resolution=resolution, + ) + + image_spec_svg.additional_properties = d + return image_spec_svg + + @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/_composition_client/models/image_spec_svg_asset_type.py b/fishjam/_composition_client/models/image_spec_svg_asset_type.py new file mode 100644 index 0000000..bdb462d --- /dev/null +++ b/fishjam/_composition_client/models/image_spec_svg_asset_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ImageSpecSvgAssetType(str, Enum): + """None""" + + SVG = "svg" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/image_type.py b/fishjam/_composition_client/models/image_type.py new file mode 100644 index 0000000..bf4d3a2 --- /dev/null +++ b/fishjam/_composition_client/models/image_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ImageType(str, Enum): + """None""" + + IMAGE = "image" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/input_stream.py b/fishjam/_composition_client/models/input_stream.py new file mode 100644 index 0000000..1d214ca --- /dev/null +++ b/fishjam/_composition_client/models/input_stream.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.input_stream_type import InputStreamType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InputStream") + + +@_attrs_define +class InputStream: + """ + Attributes: + input_id (str): + type_ (InputStreamType): + id (None | str | Unset): + """ + + input_id: str + type_: InputStreamType + id: None | str | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + input_id = self.input_id + + type_ = self.type_.value + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "input_id": input_id, + "type": type_, + }) + if id is not UNSET: + field_dict["id"] = id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + input_id = d.pop("input_id") + + type_ = InputStreamType(d.pop("type")) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + input_stream = cls( + input_id=input_id, + type_=type_, + id=id, + ) + + return input_stream diff --git a/fishjam/_composition_client/models/input_stream_type.py b/fishjam/_composition_client/models/input_stream_type.py new file mode 100644 index 0000000..ebfbcf6 --- /dev/null +++ b/fishjam/_composition_client/models/input_stream_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class InputStreamType(str, Enum): + """None""" + + INPUT_STREAM = "input_stream" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/interpolation.py b/fishjam/_composition_client/models/interpolation.py new file mode 100644 index 0000000..813f22e --- /dev/null +++ b/fishjam/_composition_client/models/interpolation.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class Interpolation(str, Enum): + """None""" + + LINEAR = "linear" + SPRING = "spring" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/mp_4_input.py b/fishjam/_composition_client/models/mp_4_input.py new file mode 100644 index 0000000..ec620a4 --- /dev/null +++ b/fishjam/_composition_client/models/mp_4_input.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.mp_4_input_type import Mp4InputType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Mp4Input") + + +@_attrs_define +class Mp4Input: + """Input stream from an MP4 file. + + Attributes: + url (str): URL of the MP4 file. + type_ (Mp4InputType): + loop (bool | None | Unset): (**default=`false`**) If input should be played in the loop. Added in v0.4.0 + """ + + url: str + type_: Mp4InputType + loop: bool | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + url = self.url + + type_ = self.type_.value + + loop: bool | None | Unset + if isinstance(self.loop, Unset): + loop = UNSET + else: + loop = self.loop + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "url": url, + "type": type_, + }) + if loop is not UNSET: + field_dict["loop"] = loop + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + url = d.pop("url") + + type_ = Mp4InputType(d.pop("type")) + + def _parse_loop(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + loop = _parse_loop(d.pop("loop", UNSET)) + + mp_4_input = cls( + url=url, + type_=type_, + loop=loop, + ) + + return mp_4_input diff --git a/fishjam/_composition_client/models/mp_4_input_type.py b/fishjam/_composition_client/models/mp_4_input_type.py new file mode 100644 index 0000000..6482917 --- /dev/null +++ b/fishjam/_composition_client/models/mp_4_input_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class Mp4InputType(str, Enum): + """None""" + + MP4 = "mp4" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/opus_encoder_preset.py b/fishjam/_composition_client/models/opus_encoder_preset.py new file mode 100644 index 0000000..e646fa1 --- /dev/null +++ b/fishjam/_composition_client/models/opus_encoder_preset.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class OpusEncoderPreset(str, Enum): + """None""" + + LOWEST_LATENCY = "lowest_latency" + QUALITY = "quality" + VOIP = "voip" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/output_end_condition.py b/fishjam/_composition_client/models/output_end_condition.py new file mode 100644 index 0000000..ef83826 --- /dev/null +++ b/fishjam/_composition_client/models/output_end_condition.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="OutputEndCondition") + + +@_attrs_define +class OutputEndCondition: + """This type defines when end of an input stream should trigger end of the output stream. Only one of those fields can + be set at the time. + Unless specified otherwise the input stream is considered finished/ended when: + - TCP connection was dropped/closed. + - RTCP Goodbye packet (`BYE`) was received. + - Mp4 track has ended. + - Input was unregistered already (or never registered). + + Attributes: + any_of (list[str] | None | Unset): Terminate output stream if any of the input streams from the list are + finished. + all_of (list[str] | None | Unset): Terminate output stream if all the input streams from the list are finished. + any_input (bool | None | Unset): Terminate output stream if any of the input streams ends. This includes streams + added after the output was registered. In particular, output stream will **not be** terminated if no inputs were + ever connected. + all_inputs (bool | None | Unset): Terminate output stream if all the input streams finish. In particular, output + stream will **be** terminated if no inputs were ever connected. + """ + + any_of: list[str] | None | Unset = UNSET + all_of: list[str] | None | Unset = UNSET + any_input: bool | None | Unset = UNSET + all_inputs: bool | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + any_of: list[str] | None | Unset + if isinstance(self.any_of, Unset): + any_of = UNSET + elif isinstance(self.any_of, list): + any_of = self.any_of + + else: + any_of = self.any_of + + all_of: list[str] | None | Unset + if isinstance(self.all_of, Unset): + all_of = UNSET + elif isinstance(self.all_of, list): + all_of = self.all_of + + else: + all_of = self.all_of + + any_input: bool | None | Unset + if isinstance(self.any_input, Unset): + any_input = UNSET + else: + any_input = self.any_input + + all_inputs: bool | None | Unset + if isinstance(self.all_inputs, Unset): + all_inputs = UNSET + else: + all_inputs = self.all_inputs + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if any_of is not UNSET: + field_dict["any_of"] = any_of + if all_of is not UNSET: + field_dict["all_of"] = all_of + if any_input is not UNSET: + field_dict["any_input"] = any_input + if all_inputs is not UNSET: + field_dict["all_inputs"] = all_inputs + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_any_of(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + any_of_type_0 = cast(list[str], data) + + return any_of_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + any_of = _parse_any_of(d.pop("any_of", UNSET)) + + def _parse_all_of(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + all_of_type_0 = cast(list[str], data) + + return all_of_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + all_of = _parse_all_of(d.pop("all_of", UNSET)) + + def _parse_any_input(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + any_input = _parse_any_input(d.pop("any_input", UNSET)) + + def _parse_all_inputs(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + all_inputs = _parse_all_inputs(d.pop("all_inputs", UNSET)) + + output_end_condition = cls( + any_of=any_of, + all_of=all_of, + any_input=any_input, + all_inputs=all_inputs, + ) + + return output_end_condition diff --git a/fishjam/_composition_client/models/output_rtmp_client_audio_options.py b/fishjam/_composition_client/models/output_rtmp_client_audio_options.py new file mode 100644 index 0000000..3aed6aa --- /dev/null +++ b/fishjam/_composition_client/models/output_rtmp_client_audio_options.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.audio_channels import AudioChannels +from ..models.audio_mixing_strategy import AudioMixingStrategy +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.audio_scene import AudioScene + from ..models.output_end_condition import OutputEndCondition + + +T = TypeVar("T", bound="OutputRtmpClientAudioOptions") + + +@_attrs_define +class OutputRtmpClientAudioOptions: + """ + Attributes: + initial (AudioScene): + mixing_strategy (AudioMixingStrategy | None | Unset): + send_eos_when (None | OutputEndCondition | Unset): + channels (AudioChannels | None | Unset): + """ + + initial: AudioScene + mixing_strategy: AudioMixingStrategy | None | Unset = UNSET + send_eos_when: None | OutputEndCondition | Unset = UNSET + channels: AudioChannels | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.output_end_condition import OutputEndCondition + + initial = self.initial.to_dict() + + mixing_strategy: None | str | Unset + if isinstance(self.mixing_strategy, Unset): + mixing_strategy = UNSET + elif isinstance(self.mixing_strategy, AudioMixingStrategy): + mixing_strategy = self.mixing_strategy.value + else: + mixing_strategy = self.mixing_strategy + + send_eos_when: dict[str, Any] | None | Unset + if isinstance(self.send_eos_when, Unset): + send_eos_when = UNSET + elif isinstance(self.send_eos_when, OutputEndCondition): + send_eos_when = self.send_eos_when.to_dict() + else: + send_eos_when = self.send_eos_when + + channels: None | str | Unset + if isinstance(self.channels, Unset): + channels = UNSET + elif isinstance(self.channels, AudioChannels): + channels = self.channels.value + else: + channels = self.channels + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "initial": initial, + }) + if mixing_strategy is not UNSET: + field_dict["mixing_strategy"] = mixing_strategy + if send_eos_when is not UNSET: + field_dict["send_eos_when"] = send_eos_when + if channels is not UNSET: + field_dict["channels"] = channels + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.audio_scene import AudioScene + from ..models.output_end_condition import OutputEndCondition + + d = dict(src_dict) + initial = AudioScene.from_dict(d.pop("initial")) + + def _parse_mixing_strategy(data: object) -> AudioMixingStrategy | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + mixing_strategy_type_1 = AudioMixingStrategy(data) + + return mixing_strategy_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AudioMixingStrategy | None | Unset, data) + + mixing_strategy = _parse_mixing_strategy(d.pop("mixing_strategy", UNSET)) + + def _parse_send_eos_when(data: object) -> None | OutputEndCondition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + send_eos_when_type_1 = OutputEndCondition.from_dict(data) + + return send_eos_when_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputEndCondition | Unset, data) + + send_eos_when = _parse_send_eos_when(d.pop("send_eos_when", UNSET)) + + def _parse_channels(data: object) -> AudioChannels | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + channels_type_1 = AudioChannels(data) + + return channels_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AudioChannels | None | Unset, data) + + channels = _parse_channels(d.pop("channels", UNSET)) + + output_rtmp_client_audio_options = cls( + initial=initial, + mixing_strategy=mixing_strategy, + send_eos_when=send_eos_when, + channels=channels, + ) + + return output_rtmp_client_audio_options diff --git a/fishjam/_composition_client/models/output_rtmp_client_video_options.py b/fishjam/_composition_client/models/output_rtmp_client_video_options.py new file mode 100644 index 0000000..0c52a14 --- /dev/null +++ b/fishjam/_composition_client/models/output_rtmp_client_video_options.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.output_end_condition import OutputEndCondition + from ..models.resolution import Resolution + from ..models.video_scene import VideoScene + + +T = TypeVar("T", bound="OutputRtmpClientVideoOptions") + + +@_attrs_define +class OutputRtmpClientVideoOptions: + """ + Attributes: + resolution (Resolution): + initial (VideoScene): + send_eos_when (None | OutputEndCondition | Unset): + """ + + resolution: Resolution + initial: VideoScene + send_eos_when: None | OutputEndCondition | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.output_end_condition import OutputEndCondition + + resolution = self.resolution.to_dict() + + initial = self.initial.to_dict() + + send_eos_when: dict[str, Any] | None | Unset + if isinstance(self.send_eos_when, Unset): + send_eos_when = UNSET + elif isinstance(self.send_eos_when, OutputEndCondition): + send_eos_when = self.send_eos_when.to_dict() + else: + send_eos_when = self.send_eos_when + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "resolution": resolution, + "initial": initial, + }) + if send_eos_when is not UNSET: + field_dict["send_eos_when"] = send_eos_when + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.output_end_condition import OutputEndCondition + from ..models.resolution import Resolution + from ..models.video_scene import VideoScene + + d = dict(src_dict) + resolution = Resolution.from_dict(d.pop("resolution")) + + initial = VideoScene.from_dict(d.pop("initial")) + + def _parse_send_eos_when(data: object) -> None | OutputEndCondition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + send_eos_when_type_1 = OutputEndCondition.from_dict(data) + + return send_eos_when_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputEndCondition | Unset, data) + + send_eos_when = _parse_send_eos_when(d.pop("send_eos_when", UNSET)) + + output_rtmp_client_video_options = cls( + resolution=resolution, + initial=initial, + send_eos_when=send_eos_when, + ) + + return output_rtmp_client_video_options diff --git a/fishjam/_composition_client/models/output_whip_audio_options.py b/fishjam/_composition_client/models/output_whip_audio_options.py new file mode 100644 index 0000000..4fa0584 --- /dev/null +++ b/fishjam/_composition_client/models/output_whip_audio_options.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.audio_channels import AudioChannels +from ..models.audio_mixing_strategy import AudioMixingStrategy +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.audio_scene import AudioScene + from ..models.output_end_condition import OutputEndCondition + from ..models.whip_audio_encoder_options_any import WhipAudioEncoderOptionsAny + from ..models.whip_audio_encoder_options_opus import WhipAudioEncoderOptionsOpus + + +T = TypeVar("T", bound="OutputWhipAudioOptions") + + +@_attrs_define +class OutputWhipAudioOptions: + """ + Attributes: + initial (AudioScene): + mixing_strategy (AudioMixingStrategy | None | Unset): + send_eos_when (None | OutputEndCondition | Unset): + channels (AudioChannels | None | Unset): + encoder_preferences (list[WhipAudioEncoderOptionsAny | WhipAudioEncoderOptionsOpus] | None | Unset): Codec + preferences list. + """ + + initial: AudioScene + mixing_strategy: AudioMixingStrategy | None | Unset = UNSET + send_eos_when: None | OutputEndCondition | Unset = UNSET + channels: AudioChannels | None | Unset = UNSET + encoder_preferences: ( + list[WhipAudioEncoderOptionsAny | WhipAudioEncoderOptionsOpus] | None | Unset + ) = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.output_end_condition import OutputEndCondition + from ..models.whip_audio_encoder_options_opus import WhipAudioEncoderOptionsOpus + + initial = self.initial.to_dict() + + mixing_strategy: None | str | Unset + if isinstance(self.mixing_strategy, Unset): + mixing_strategy = UNSET + elif isinstance(self.mixing_strategy, AudioMixingStrategy): + mixing_strategy = self.mixing_strategy.value + else: + mixing_strategy = self.mixing_strategy + + send_eos_when: dict[str, Any] | None | Unset + if isinstance(self.send_eos_when, Unset): + send_eos_when = UNSET + elif isinstance(self.send_eos_when, OutputEndCondition): + send_eos_when = self.send_eos_when.to_dict() + else: + send_eos_when = self.send_eos_when + + channels: None | str | Unset + if isinstance(self.channels, Unset): + channels = UNSET + elif isinstance(self.channels, AudioChannels): + channels = self.channels.value + else: + channels = self.channels + + encoder_preferences: list[dict[str, Any]] | None | Unset + if isinstance(self.encoder_preferences, Unset): + encoder_preferences = UNSET + elif isinstance(self.encoder_preferences, list): + encoder_preferences = [] + for encoder_preferences_type_0_item_data in self.encoder_preferences: + encoder_preferences_type_0_item: dict[str, Any] + if isinstance( + encoder_preferences_type_0_item_data, WhipAudioEncoderOptionsOpus + ): + encoder_preferences_type_0_item = ( + encoder_preferences_type_0_item_data.to_dict() + ) + else: + encoder_preferences_type_0_item = ( + encoder_preferences_type_0_item_data.to_dict() + ) + + encoder_preferences.append(encoder_preferences_type_0_item) + + else: + encoder_preferences = self.encoder_preferences + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "initial": initial, + }) + if mixing_strategy is not UNSET: + field_dict["mixing_strategy"] = mixing_strategy + if send_eos_when is not UNSET: + field_dict["send_eos_when"] = send_eos_when + if channels is not UNSET: + field_dict["channels"] = channels + if encoder_preferences is not UNSET: + field_dict["encoder_preferences"] = encoder_preferences + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.audio_scene import AudioScene + from ..models.output_end_condition import OutputEndCondition + from ..models.whip_audio_encoder_options_any import WhipAudioEncoderOptionsAny + from ..models.whip_audio_encoder_options_opus import WhipAudioEncoderOptionsOpus + + d = dict(src_dict) + initial = AudioScene.from_dict(d.pop("initial")) + + def _parse_mixing_strategy(data: object) -> AudioMixingStrategy | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + mixing_strategy_type_1 = AudioMixingStrategy(data) + + return mixing_strategy_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AudioMixingStrategy | None | Unset, data) + + mixing_strategy = _parse_mixing_strategy(d.pop("mixing_strategy", UNSET)) + + def _parse_send_eos_when(data: object) -> None | OutputEndCondition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + send_eos_when_type_1 = OutputEndCondition.from_dict(data) + + return send_eos_when_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputEndCondition | Unset, data) + + send_eos_when = _parse_send_eos_when(d.pop("send_eos_when", UNSET)) + + def _parse_channels(data: object) -> AudioChannels | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + channels_type_1 = AudioChannels(data) + + return channels_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AudioChannels | None | Unset, data) + + channels = _parse_channels(d.pop("channels", UNSET)) + + def _parse_encoder_preferences( + data: object, + ) -> ( + list[WhipAudioEncoderOptionsAny | WhipAudioEncoderOptionsOpus] + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + encoder_preferences_type_0 = [] + _encoder_preferences_type_0 = data + for encoder_preferences_type_0_item_data in _encoder_preferences_type_0: + + def _parse_encoder_preferences_type_0_item( + data: object, + ) -> WhipAudioEncoderOptionsAny | WhipAudioEncoderOptionsOpus: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_whip_audio_encoder_options_whip_audio_encoder_options_opus = WhipAudioEncoderOptionsOpus.from_dict( + data + ) + + return componentsschemas_whip_audio_encoder_options_whip_audio_encoder_options_opus + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_whip_audio_encoder_options_whip_audio_encoder_options_any = WhipAudioEncoderOptionsAny.from_dict( + data + ) + + return componentsschemas_whip_audio_encoder_options_whip_audio_encoder_options_any + + encoder_preferences_type_0_item = ( + _parse_encoder_preferences_type_0_item( + encoder_preferences_type_0_item_data + ) + ) + + encoder_preferences_type_0.append(encoder_preferences_type_0_item) + + return encoder_preferences_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + list[WhipAudioEncoderOptionsAny | WhipAudioEncoderOptionsOpus] + | None + | Unset, + data, + ) + + encoder_preferences = _parse_encoder_preferences( + d.pop("encoder_preferences", UNSET) + ) + + output_whip_audio_options = cls( + initial=initial, + mixing_strategy=mixing_strategy, + send_eos_when=send_eos_when, + channels=channels, + encoder_preferences=encoder_preferences, + ) + + return output_whip_audio_options diff --git a/fishjam/_composition_client/models/output_whip_video_options.py b/fishjam/_composition_client/models/output_whip_video_options.py new file mode 100644 index 0000000..d47638b --- /dev/null +++ b/fishjam/_composition_client/models/output_whip_video_options.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.output_end_condition import OutputEndCondition + from ..models.resolution import Resolution + from ..models.video_scene import VideoScene + + +T = TypeVar("T", bound="OutputWhipVideoOptions") + + +@_attrs_define +class OutputWhipVideoOptions: + """ + Attributes: + resolution (Resolution): + initial (VideoScene): + send_eos_when (None | OutputEndCondition | Unset): + """ + + resolution: Resolution + initial: VideoScene + send_eos_when: None | OutputEndCondition | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.output_end_condition import OutputEndCondition + + resolution = self.resolution.to_dict() + + initial = self.initial.to_dict() + + send_eos_when: dict[str, Any] | None | Unset + if isinstance(self.send_eos_when, Unset): + send_eos_when = UNSET + elif isinstance(self.send_eos_when, OutputEndCondition): + send_eos_when = self.send_eos_when.to_dict() + else: + send_eos_when = self.send_eos_when + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "resolution": resolution, + "initial": initial, + }) + if send_eos_when is not UNSET: + field_dict["send_eos_when"] = send_eos_when + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.output_end_condition import OutputEndCondition + from ..models.resolution import Resolution + from ..models.video_scene import VideoScene + + d = dict(src_dict) + resolution = Resolution.from_dict(d.pop("resolution")) + + initial = VideoScene.from_dict(d.pop("initial")) + + def _parse_send_eos_when(data: object) -> None | OutputEndCondition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + send_eos_when_type_1 = OutputEndCondition.from_dict(data) + + return send_eos_when_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputEndCondition | Unset, data) + + send_eos_when = _parse_send_eos_when(d.pop("send_eos_when", UNSET)) + + output_whip_video_options = cls( + resolution=resolution, + initial=initial, + send_eos_when=send_eos_when, + ) + + return output_whip_video_options diff --git a/fishjam/_composition_client/models/overflow.py b/fishjam/_composition_client/models/overflow.py new file mode 100644 index 0000000..2b38466 --- /dev/null +++ b/fishjam/_composition_client/models/overflow.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class Overflow(str, Enum): + """None""" + + FIT = "fit" + HIDDEN = "hidden" + VISIBLE = "visible" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/pixel_format.py b/fishjam/_composition_client/models/pixel_format.py new file mode 100644 index 0000000..d22422d --- /dev/null +++ b/fishjam/_composition_client/models/pixel_format.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class PixelFormat(str, Enum): + """None""" + + YUV420P = "yuv420p" + YUV422P = "yuv422p" + YUV444P = "yuv444p" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/register_font_body.py b/fishjam/_composition_client/models/register_font_body.py new file mode 100644 index 0000000..96d1ff5 --- /dev/null +++ b/fishjam/_composition_client/models/register_font_body.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from io import BytesIO +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..types import File + +T = TypeVar("T", bound="RegisterFontBody") + + +@_attrs_define +class RegisterFontBody: + """ + Attributes: + font (File): + """ + + font: File + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + font = self.font.to_tuple() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "font": font, + }) + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + files.append(("font", self.font.to_tuple())) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + font = File(payload=BytesIO(d.pop("font"))) + + register_font_body = cls( + font=font, + ) + + register_font_body.additional_properties = d + return register_font_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 diff --git a/fishjam/_composition_client/models/register_input_response.py b/fishjam/_composition_client/models/register_input_response.py new file mode 100644 index 0000000..da66255 --- /dev/null +++ b/fishjam/_composition_client/models/register_input_response.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="RegisterInputResponse") + + +@_attrs_define +class RegisterInputResponse: + """ + Attributes: + bearer_token (None | str | Unset): + endpoint_route (None | str | Unset): + video_duration_ms (int | None | Unset): + audio_duration_ms (int | None | Unset): + port (int | None | Unset): + """ + + bearer_token: None | str | Unset = UNSET + endpoint_route: None | str | Unset = UNSET + video_duration_ms: int | None | Unset = UNSET + audio_duration_ms: int | None | Unset = UNSET + port: int | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + bearer_token: None | str | Unset + if isinstance(self.bearer_token, Unset): + bearer_token = UNSET + else: + bearer_token = self.bearer_token + + endpoint_route: None | str | Unset + if isinstance(self.endpoint_route, Unset): + endpoint_route = UNSET + else: + endpoint_route = self.endpoint_route + + video_duration_ms: int | None | Unset + if isinstance(self.video_duration_ms, Unset): + video_duration_ms = UNSET + else: + video_duration_ms = self.video_duration_ms + + audio_duration_ms: int | None | Unset + if isinstance(self.audio_duration_ms, Unset): + audio_duration_ms = UNSET + else: + audio_duration_ms = self.audio_duration_ms + + port: int | None | Unset + if isinstance(self.port, Unset): + port = UNSET + else: + port = self.port + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if bearer_token is not UNSET: + field_dict["bearer_token"] = bearer_token + if endpoint_route is not UNSET: + field_dict["endpoint_route"] = endpoint_route + if video_duration_ms is not UNSET: + field_dict["video_duration_ms"] = video_duration_ms + if audio_duration_ms is not UNSET: + field_dict["audio_duration_ms"] = audio_duration_ms + if port is not UNSET: + field_dict["port"] = port + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_bearer_token(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + bearer_token = _parse_bearer_token(d.pop("bearer_token", UNSET)) + + def _parse_endpoint_route(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + endpoint_route = _parse_endpoint_route(d.pop("endpoint_route", UNSET)) + + def _parse_video_duration_ms(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + video_duration_ms = _parse_video_duration_ms(d.pop("video_duration_ms", UNSET)) + + def _parse_audio_duration_ms(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + audio_duration_ms = _parse_audio_duration_ms(d.pop("audio_duration_ms", UNSET)) + + def _parse_port(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + port = _parse_port(d.pop("port", UNSET)) + + register_input_response = cls( + bearer_token=bearer_token, + endpoint_route=endpoint_route, + video_duration_ms=video_duration_ms, + audio_duration_ms=audio_duration_ms, + port=port, + ) + + return register_input_response diff --git a/fishjam/_composition_client/models/register_template_output.py b/fishjam/_composition_client/models/register_template_output.py new file mode 100644 index 0000000..bb2a25b --- /dev/null +++ b/fishjam/_composition_client/models/register_template_output.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from collections.abc import Mapping +from io import BytesIO +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import File + +if TYPE_CHECKING: + from ..models.rtmp_output import RtmpOutput + from ..models.whip_output import WhipOutput + + +T = TypeVar("T", bound="RegisterTemplateOutput") + + +@_attrs_define +class RegisterTemplateOutput: + """ + Attributes: + config (RtmpOutput | WhipOutput): + template (File): + """ + + config: RtmpOutput | WhipOutput + template: File + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.rtmp_output import RtmpOutput + + config: dict[str, Any] + if isinstance(self.config, RtmpOutput): + config = self.config.to_dict() + else: + config = self.config.to_dict() + + template = self.template.to_tuple() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "config": config, + "template": template, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.rtmp_output import RtmpOutput + from ..models.whip_output import WhipOutput + + d = dict(src_dict) + + def _parse_config(data: object) -> RtmpOutput | WhipOutput: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_register_output_type_0 = RtmpOutput.from_dict(data) + + return componentsschemas_register_output_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_register_output_type_1 = WhipOutput.from_dict(data) + + return componentsschemas_register_output_type_1 + + config = _parse_config(d.pop("config")) + + template = File(payload=BytesIO(d.pop("template"))) + + register_template_output = cls( + config=config, + template=template, + ) + + register_template_output.additional_properties = d + return register_template_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/_composition_client/models/register_template_output_body.py b/fishjam/_composition_client/models/register_template_output_body.py new file mode 100644 index 0000000..75d6c95 --- /dev/null +++ b/fishjam/_composition_client/models/register_template_output_body.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import json +from collections.abc import Mapping +from io import BytesIO +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..types import File + +if TYPE_CHECKING: + from ..models.rtmp_output import RtmpOutput + from ..models.whip_output import WhipOutput + + +T = TypeVar("T", bound="RegisterTemplateOutputBody") + + +@_attrs_define +class RegisterTemplateOutputBody: + """ + Attributes: + config (RtmpOutput | WhipOutput): + template (File): + """ + + config: RtmpOutput | WhipOutput + template: File + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.rtmp_output import RtmpOutput + + config: dict[str, Any] + if isinstance(self.config, RtmpOutput): + config = self.config.to_dict() + else: + config = self.config.to_dict() + + template = self.template.to_tuple() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "config": config, + "template": template, + }) + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + from ..models.rtmp_output import RtmpOutput + + files: types.RequestFiles = [] + + if isinstance(self.config, RtmpOutput): + files.append(( + "config", + (None, json.dumps(self.config.to_dict()).encode(), "application/json"), + )) + else: + files.append(( + "config", + (None, json.dumps(self.config.to_dict()).encode(), "application/json"), + )) + + files.append(("template", self.template.to_tuple())) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.rtmp_output import RtmpOutput + from ..models.whip_output import WhipOutput + + d = dict(src_dict) + + def _parse_config(data: object) -> RtmpOutput | WhipOutput: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_register_output_type_0 = RtmpOutput.from_dict(data) + + return componentsschemas_register_output_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_register_output_type_1 = WhipOutput.from_dict(data) + + return componentsschemas_register_output_type_1 + + config = _parse_config(d.pop("config")) + + template = File(payload=BytesIO(d.pop("template"))) + + register_template_output_body = cls( + config=config, + template=template, + ) + + register_template_output_body.additional_properties = d + return register_template_output_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 diff --git a/fishjam/_composition_client/models/rescale_mode.py b/fishjam/_composition_client/models/rescale_mode.py new file mode 100644 index 0000000..465ca1b --- /dev/null +++ b/fishjam/_composition_client/models/rescale_mode.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class RescaleMode(str, Enum): + """None""" + + FILL = "fill" + FIT = "fit" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/rescaler.py b/fishjam/_composition_client/models/rescaler.py new file mode 100644 index 0000000..b320a96 --- /dev/null +++ b/fishjam/_composition_client/models/rescaler.py @@ -0,0 +1,544 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.horizontal_align import HorizontalAlign +from ..models.rescale_mode import RescaleMode +from ..models.rescaler_type import RescalerType +from ..models.vertical_align import VerticalAlign +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.box_shadow import BoxShadow + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.transition import Transition + from ..models.view import View + + +T = TypeVar("T", bound="Rescaler") + + +@_attrs_define +class Rescaler: + """ + Attributes: + child (Image | InputStream | Rescaler | Text | Tiles | View): + type_ (RescalerType): + id (None | str | Unset): + mode (None | RescaleMode | Unset): + horizontal_align (HorizontalAlign | None | Unset): + vertical_align (None | Unset | VerticalAlign): + width (float | None | Unset): Width of a component in pixels (without a border). Exact behavior might be + different + based on the parent component: + - If the parent component is a layout, check sections "Absolute positioning" and "Static + positioning" of that component. + - If the parent component is not a layout, then this field is required. + height (float | None | Unset): Height of a component in pixels (without a border). Exact behavior might be + different + based on the parent component: + - If the parent component is a layout, check sections "Absolute positioning" and "Static + positioning" of that component. + - If the parent component is not a layout, then this field is required. + top (float | None | Unset): Distance in pixels between this component's top edge and its parent's top edge + (including a border). + If this field is defined, then the component will ignore a layout defined by its parent. + left (float | None | Unset): Distance in pixels between this component's left edge and its parent's left edge + (including a border). + If this field is defined, this element will be absolutely positioned, instead of being + laid out by its parent. + bottom (float | None | Unset): Distance in pixels between the bottom edge of this component and the bottom edge + of its + parent (including a border). If this field is defined, this element will be absolutely + positioned, instead of being laid out by its parent. + right (float | None | Unset): Distance in pixels between this component's right edge and its parent's right + edge. + If this field is defined, this element will be absolutely positioned, instead of being + laid out by its parent. + rotation (float | None | Unset): Rotation of a component in degrees. If this field is defined, this element will + be + absolutely positioned, instead of being laid out by its parent. + transition (None | Transition | Unset): + border_radius (float | None | Unset): (**default=`0.0`**) Radius of a rounded corner. + border_width (float | None | Unset): (**default=`0.0`**) Border width. + border_color (None | str | Unset): + box_shadow (list[BoxShadow] | None | Unset): List of box shadows. + """ + + child: Image | InputStream | Rescaler | Text | Tiles | View + type_: RescalerType + id: None | str | Unset = UNSET + mode: None | RescaleMode | Unset = UNSET + horizontal_align: HorizontalAlign | None | Unset = UNSET + vertical_align: None | Unset | VerticalAlign = UNSET + width: float | None | Unset = UNSET + height: float | None | Unset = UNSET + top: float | None | Unset = UNSET + left: float | None | Unset = UNSET + bottom: float | None | Unset = UNSET + right: float | None | Unset = UNSET + rotation: float | None | Unset = UNSET + transition: None | Transition | Unset = UNSET + border_radius: float | None | Unset = UNSET + border_width: float | None | Unset = UNSET + border_color: None | str | Unset = UNSET + box_shadow: list[BoxShadow] | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.input_stream import InputStream + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.transition import Transition + from ..models.view import View + + child: dict[str, Any] + if isinstance(self.child, InputStream): + child = self.child.to_dict() + elif isinstance(self.child, View): + child = self.child.to_dict() + elif isinstance(self.child, Text): + child = self.child.to_dict() + elif isinstance(self.child, Tiles): + child = self.child.to_dict() + elif isinstance(self.child, Rescaler): + child = self.child.to_dict() + else: + child = self.child.to_dict() + + type_ = self.type_.value + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + mode: None | str | Unset + if isinstance(self.mode, Unset): + mode = UNSET + elif isinstance(self.mode, RescaleMode): + mode = self.mode.value + else: + mode = self.mode + + horizontal_align: None | str | Unset + if isinstance(self.horizontal_align, Unset): + horizontal_align = UNSET + elif isinstance(self.horizontal_align, HorizontalAlign): + horizontal_align = self.horizontal_align.value + else: + horizontal_align = self.horizontal_align + + vertical_align: None | str | Unset + if isinstance(self.vertical_align, Unset): + vertical_align = UNSET + elif isinstance(self.vertical_align, VerticalAlign): + vertical_align = self.vertical_align.value + else: + vertical_align = self.vertical_align + + width: float | None | Unset + if isinstance(self.width, Unset): + width = UNSET + else: + width = self.width + + height: float | None | Unset + if isinstance(self.height, Unset): + height = UNSET + else: + height = self.height + + top: float | None | Unset + if isinstance(self.top, Unset): + top = UNSET + else: + top = self.top + + left: float | None | Unset + if isinstance(self.left, Unset): + left = UNSET + else: + left = self.left + + bottom: float | None | Unset + if isinstance(self.bottom, Unset): + bottom = UNSET + else: + bottom = self.bottom + + right: float | None | Unset + if isinstance(self.right, Unset): + right = UNSET + else: + right = self.right + + rotation: float | None | Unset + if isinstance(self.rotation, Unset): + rotation = UNSET + else: + rotation = self.rotation + + transition: dict[str, Any] | None | Unset + if isinstance(self.transition, Unset): + transition = UNSET + elif isinstance(self.transition, Transition): + transition = self.transition.to_dict() + else: + transition = self.transition + + border_radius: float | None | Unset + if isinstance(self.border_radius, Unset): + border_radius = UNSET + else: + border_radius = self.border_radius + + border_width: float | None | Unset + if isinstance(self.border_width, Unset): + border_width = UNSET + else: + border_width = self.border_width + + border_color: None | str | Unset + if isinstance(self.border_color, Unset): + border_color = UNSET + else: + border_color = self.border_color + + box_shadow: list[dict[str, Any]] | None | Unset + if isinstance(self.box_shadow, Unset): + box_shadow = UNSET + elif isinstance(self.box_shadow, list): + box_shadow = [] + for box_shadow_type_0_item_data in self.box_shadow: + box_shadow_type_0_item = box_shadow_type_0_item_data.to_dict() + box_shadow.append(box_shadow_type_0_item) + + else: + box_shadow = self.box_shadow + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "child": child, + "type": type_, + }) + if id is not UNSET: + field_dict["id"] = id + if mode is not UNSET: + field_dict["mode"] = mode + if horizontal_align is not UNSET: + field_dict["horizontal_align"] = horizontal_align + if vertical_align is not UNSET: + field_dict["vertical_align"] = vertical_align + if width is not UNSET: + field_dict["width"] = width + if height is not UNSET: + field_dict["height"] = height + if top is not UNSET: + field_dict["top"] = top + if left is not UNSET: + field_dict["left"] = left + if bottom is not UNSET: + field_dict["bottom"] = bottom + if right is not UNSET: + field_dict["right"] = right + if rotation is not UNSET: + field_dict["rotation"] = rotation + if transition is not UNSET: + field_dict["transition"] = transition + if border_radius is not UNSET: + field_dict["border_radius"] = border_radius + if border_width is not UNSET: + field_dict["border_width"] = border_width + if border_color is not UNSET: + field_dict["border_color"] = border_color + if box_shadow is not UNSET: + field_dict["box_shadow"] = box_shadow + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.box_shadow import BoxShadow + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.transition import Transition + from ..models.view import View + + d = dict(src_dict) + + def _parse_child( + data: object, + ) -> Image | InputStream | Rescaler | Text | Tiles | View: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_0 = InputStream.from_dict(data) + + return componentsschemas_component_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_1 = View.from_dict(data) + + return componentsschemas_component_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_2 = Text.from_dict(data) + + return componentsschemas_component_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_3 = Tiles.from_dict(data) + + return componentsschemas_component_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_4 = Rescaler.from_dict(data) + + return componentsschemas_component_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_5 = Image.from_dict(data) + + return componentsschemas_component_type_5 + + child = _parse_child(d.pop("child")) + + type_ = RescalerType(d.pop("type")) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_mode(data: object) -> None | RescaleMode | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + mode_type_1 = RescaleMode(data) + + return mode_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | RescaleMode | Unset, data) + + mode = _parse_mode(d.pop("mode", UNSET)) + + def _parse_horizontal_align(data: object) -> HorizontalAlign | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + horizontal_align_type_1 = HorizontalAlign(data) + + return horizontal_align_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(HorizontalAlign | None | Unset, data) + + horizontal_align = _parse_horizontal_align(d.pop("horizontal_align", UNSET)) + + def _parse_vertical_align(data: object) -> None | Unset | VerticalAlign: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + vertical_align_type_1 = VerticalAlign(data) + + return vertical_align_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | VerticalAlign, data) + + vertical_align = _parse_vertical_align(d.pop("vertical_align", UNSET)) + + def _parse_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + width = _parse_width(d.pop("width", UNSET)) + + def _parse_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + height = _parse_height(d.pop("height", UNSET)) + + def _parse_top(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + top = _parse_top(d.pop("top", UNSET)) + + def _parse_left(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + left = _parse_left(d.pop("left", UNSET)) + + def _parse_bottom(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + bottom = _parse_bottom(d.pop("bottom", UNSET)) + + def _parse_right(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + right = _parse_right(d.pop("right", UNSET)) + + def _parse_rotation(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + rotation = _parse_rotation(d.pop("rotation", UNSET)) + + def _parse_transition(data: object) -> None | Transition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + transition_type_1 = Transition.from_dict(data) + + return transition_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Transition | Unset, data) + + transition = _parse_transition(d.pop("transition", UNSET)) + + def _parse_border_radius(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + border_radius = _parse_border_radius(d.pop("border_radius", UNSET)) + + def _parse_border_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + border_width = _parse_border_width(d.pop("border_width", UNSET)) + + def _parse_border_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + border_color = _parse_border_color(d.pop("border_color", UNSET)) + + def _parse_box_shadow(data: object) -> list[BoxShadow] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + box_shadow_type_0 = [] + _box_shadow_type_0 = data + for box_shadow_type_0_item_data in _box_shadow_type_0: + box_shadow_type_0_item = BoxShadow.from_dict( + box_shadow_type_0_item_data + ) + + box_shadow_type_0.append(box_shadow_type_0_item) + + return box_shadow_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[BoxShadow] | None | Unset, data) + + box_shadow = _parse_box_shadow(d.pop("box_shadow", UNSET)) + + rescaler = cls( + child=child, + type_=type_, + id=id, + mode=mode, + horizontal_align=horizontal_align, + vertical_align=vertical_align, + width=width, + height=height, + top=top, + left=left, + bottom=bottom, + right=right, + rotation=rotation, + transition=transition, + border_radius=border_radius, + border_width=border_width, + border_color=border_color, + box_shadow=box_shadow, + ) + + return rescaler diff --git a/fishjam/_composition_client/models/rescaler_type.py b/fishjam/_composition_client/models/rescaler_type.py new file mode 100644 index 0000000..8d565a8 --- /dev/null +++ b/fishjam/_composition_client/models/rescaler_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class RescalerType(str, Enum): + """None""" + + RESCALER = "rescaler" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/resolution.py b/fishjam/_composition_client/models/resolution.py new file mode 100644 index 0000000..2587893 --- /dev/null +++ b/fishjam/_composition_client/models/resolution.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="Resolution") + + +@_attrs_define +class Resolution: + """ + Attributes: + width (int): Width in pixels. + height (int): Height in pixels. + """ + + width: int + height: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + width = self.width + + height = self.height + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "width": width, + "height": height, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + width = d.pop("width") + + height = d.pop("height") + + resolution = cls( + width=width, + height=height, + ) + + resolution.additional_properties = d + return resolution + + @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/_composition_client/models/rtmp_input.py b/fishjam/_composition_client/models/rtmp_input.py new file mode 100644 index 0000000..73e5a54 --- /dev/null +++ b/fishjam/_composition_client/models/rtmp_input.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +from ..models.rtmp_input_type import RtmpInputType + +T = TypeVar("T", bound="RtmpInput") + + +@_attrs_define +class RtmpInput: + """ + Attributes: + stream_key (str): The RTMP stream key. + This is the path segment of the RTMP stream URL that Smelter listens on for incoming streams. + Format: `rtmp://:/` + type_ (RtmpInputType): + """ + + stream_key: str + type_: RtmpInputType + + def to_dict(self) -> dict[str, Any]: + stream_key = self.stream_key + + type_ = self.type_.value + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "stream_key": stream_key, + "type": type_, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + stream_key = d.pop("stream_key") + + type_ = RtmpInputType(d.pop("type")) + + rtmp_input = cls( + stream_key=stream_key, + type_=type_, + ) + + return rtmp_input diff --git a/fishjam/_composition_client/models/rtmp_input_type.py b/fishjam/_composition_client/models/rtmp_input_type.py new file mode 100644 index 0000000..1b652fa --- /dev/null +++ b/fishjam/_composition_client/models/rtmp_input_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class RtmpInputType(str, Enum): + """None""" + + RTMP_SERVER = "rtmp_server" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/rtmp_output.py b/fishjam/_composition_client/models/rtmp_output.py new file mode 100644 index 0000000..034b313 --- /dev/null +++ b/fishjam/_composition_client/models/rtmp_output.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.rtmp_output_type import RtmpOutputType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.output_rtmp_client_audio_options import OutputRtmpClientAudioOptions + from ..models.output_rtmp_client_video_options import OutputRtmpClientVideoOptions + + +T = TypeVar("T", bound="RtmpOutput") + + +@_attrs_define +class RtmpOutput: + """ + Attributes: + url (str): RTMP endpoint url. + type_ (RtmpOutputType): + video (None | OutputRtmpClientVideoOptions | Unset): + audio (None | OutputRtmpClientAudioOptions | Unset): + """ + + url: str + type_: RtmpOutputType + video: None | OutputRtmpClientVideoOptions | Unset = UNSET + audio: None | OutputRtmpClientAudioOptions | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.output_rtmp_client_audio_options import ( + OutputRtmpClientAudioOptions, + ) + from ..models.output_rtmp_client_video_options import ( + OutputRtmpClientVideoOptions, + ) + + url = self.url + + type_ = self.type_.value + + video: dict[str, Any] | None | Unset + if isinstance(self.video, Unset): + video = UNSET + elif isinstance(self.video, OutputRtmpClientVideoOptions): + video = self.video.to_dict() + else: + video = self.video + + audio: dict[str, Any] | None | Unset + if isinstance(self.audio, Unset): + audio = UNSET + elif isinstance(self.audio, OutputRtmpClientAudioOptions): + audio = self.audio.to_dict() + else: + audio = self.audio + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "url": url, + "type": type_, + }) + if video is not UNSET: + field_dict["video"] = video + if audio is not UNSET: + field_dict["audio"] = audio + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.output_rtmp_client_audio_options import ( + OutputRtmpClientAudioOptions, + ) + from ..models.output_rtmp_client_video_options import ( + OutputRtmpClientVideoOptions, + ) + + d = dict(src_dict) + url = d.pop("url") + + type_ = RtmpOutputType(d.pop("type")) + + def _parse_video(data: object) -> None | OutputRtmpClientVideoOptions | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + video_type_1 = OutputRtmpClientVideoOptions.from_dict(data) + + return video_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputRtmpClientVideoOptions | Unset, data) + + video = _parse_video(d.pop("video", UNSET)) + + def _parse_audio(data: object) -> None | OutputRtmpClientAudioOptions | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + audio_type_1 = OutputRtmpClientAudioOptions.from_dict(data) + + return audio_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputRtmpClientAudioOptions | Unset, data) + + audio = _parse_audio(d.pop("audio", UNSET)) + + rtmp_output = cls( + url=url, + type_=type_, + video=video, + audio=audio, + ) + + return rtmp_output diff --git a/fishjam/_composition_client/models/rtmp_output_type.py b/fishjam/_composition_client/models/rtmp_output_type.py new file mode 100644 index 0000000..9b5e26d --- /dev/null +++ b/fishjam/_composition_client/models/rtmp_output_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class RtmpOutputType(str, Enum): + """None""" + + RTMP_CLIENT = "rtmp_client" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/send_composition_event_body.py b/fishjam/_composition_client/models/send_composition_event_body.py new file mode 100644 index 0000000..d4b8193 --- /dev/null +++ b/fishjam/_composition_client/models/send_composition_event_body.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="SendCompositionEventBody") + + +@_attrs_define +class SendCompositionEventBody: + """ + Attributes: + event_name (str): Name of the event delivered to the composition's templates. Example: START_LIVE. + data (Any | Unset): Optional arbitrary JSON payload delivered with the event. + """ + + event_name: str + data: Any | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + event_name = self.event_name + + data = self.data + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "event_name": event_name, + }) + if data is not UNSET: + field_dict["data"] = data + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + event_name = d.pop("event_name") + + data = d.pop("data", UNSET) + + send_composition_event_body = cls( + event_name=event_name, + data=data, + ) + + return send_composition_event_body diff --git a/fishjam/_composition_client/models/text.py b/fishjam/_composition_client/models/text.py new file mode 100644 index 0000000..2cf50c7 --- /dev/null +++ b/fishjam/_composition_client/models/text.py @@ -0,0 +1,375 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.horizontal_align import HorizontalAlign +from ..models.text_style import TextStyle +from ..models.text_type import TextType +from ..models.text_weight import TextWeight +from ..models.text_wrap_mode import TextWrapMode +from ..types import UNSET, Unset + +T = TypeVar("T", bound="Text") + + +@_attrs_define +class Text: + """ + Attributes: + text (str): Text that will be rendered. + font_size (float): Font size in pixels. + type_ (TextType): + id (None | str | Unset): + width (float | None | Unset): Width of a texture that text will be rendered on. If not provided, the resulting + texture + will be sized based on the defined text but limited to `max_width` value. + height (float | None | Unset): Height of a texture that text will be rendered on. If not provided, the resulting + texture + will be sized based on the defined text but limited to `max_height` value. + It's an error to provide `height` if `width` is not defined. + max_width (float | None | Unset): (**default=`7682`**) Maximal `width`. Limits the width of the texture that the + text will be rendered on. + Value is ignored if `width` is defined. + max_height (float | None | Unset): (**default=`4320`**) Maximal `height`. Limits the height of the texture that + the text will be rendered on. + Value is ignored if height is defined. + line_height (float | None | Unset): Distance between lines in pixels. Defaults to the value of the `font_size` + property. + color (None | str | Unset): + background_color (None | str | Unset): + font_family (None | str | Unset): (**default=`"Verdana"`**) Font family. Provide [family- + name](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#family-name-value) + for a specific font. "generic-family" values like e.g. "sans-serif" will not work. + style (None | TextStyle | Unset): + align (HorizontalAlign | None | Unset): + wrap (None | TextWrapMode | Unset): + weight (None | TextWeight | Unset): + """ + + text: str + font_size: float + type_: TextType + id: None | str | Unset = UNSET + width: float | None | Unset = UNSET + height: float | None | Unset = UNSET + max_width: float | None | Unset = UNSET + max_height: float | None | Unset = UNSET + line_height: float | None | Unset = UNSET + color: None | str | Unset = UNSET + background_color: None | str | Unset = UNSET + font_family: None | str | Unset = UNSET + style: None | TextStyle | Unset = UNSET + align: HorizontalAlign | None | Unset = UNSET + wrap: None | TextWrapMode | Unset = UNSET + weight: None | TextWeight | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + text = self.text + + font_size = self.font_size + + type_ = self.type_.value + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + width: float | None | Unset + if isinstance(self.width, Unset): + width = UNSET + else: + width = self.width + + height: float | None | Unset + if isinstance(self.height, Unset): + height = UNSET + else: + height = self.height + + max_width: float | None | Unset + if isinstance(self.max_width, Unset): + max_width = UNSET + else: + max_width = self.max_width + + max_height: float | None | Unset + if isinstance(self.max_height, Unset): + max_height = UNSET + else: + max_height = self.max_height + + line_height: float | None | Unset + if isinstance(self.line_height, Unset): + line_height = UNSET + else: + line_height = self.line_height + + color: None | str | Unset + if isinstance(self.color, Unset): + color = UNSET + else: + color = self.color + + background_color: None | str | Unset + if isinstance(self.background_color, Unset): + background_color = UNSET + else: + background_color = self.background_color + + font_family: None | str | Unset + if isinstance(self.font_family, Unset): + font_family = UNSET + else: + font_family = self.font_family + + style: None | str | Unset + if isinstance(self.style, Unset): + style = UNSET + elif isinstance(self.style, TextStyle): + style = self.style.value + else: + style = self.style + + align: None | str | Unset + if isinstance(self.align, Unset): + align = UNSET + elif isinstance(self.align, HorizontalAlign): + align = self.align.value + else: + align = self.align + + wrap: None | str | Unset + if isinstance(self.wrap, Unset): + wrap = UNSET + elif isinstance(self.wrap, TextWrapMode): + wrap = self.wrap.value + else: + wrap = self.wrap + + weight: None | str | Unset + if isinstance(self.weight, Unset): + weight = UNSET + elif isinstance(self.weight, TextWeight): + weight = self.weight.value + else: + weight = self.weight + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "text": text, + "font_size": font_size, + "type": type_, + }) + if id is not UNSET: + field_dict["id"] = id + if width is not UNSET: + field_dict["width"] = width + if height is not UNSET: + field_dict["height"] = height + if max_width is not UNSET: + field_dict["max_width"] = max_width + if max_height is not UNSET: + field_dict["max_height"] = max_height + if line_height is not UNSET: + field_dict["line_height"] = line_height + if color is not UNSET: + field_dict["color"] = color + if background_color is not UNSET: + field_dict["background_color"] = background_color + if font_family is not UNSET: + field_dict["font_family"] = font_family + if style is not UNSET: + field_dict["style"] = style + if align is not UNSET: + field_dict["align"] = align + if wrap is not UNSET: + field_dict["wrap"] = wrap + if weight is not UNSET: + field_dict["weight"] = weight + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + text = d.pop("text") + + font_size = d.pop("font_size") + + type_ = TextType(d.pop("type")) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + width = _parse_width(d.pop("width", UNSET)) + + def _parse_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + height = _parse_height(d.pop("height", UNSET)) + + def _parse_max_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + max_width = _parse_max_width(d.pop("max_width", UNSET)) + + def _parse_max_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + max_height = _parse_max_height(d.pop("max_height", UNSET)) + + def _parse_line_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + line_height = _parse_line_height(d.pop("line_height", UNSET)) + + def _parse_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + color = _parse_color(d.pop("color", UNSET)) + + def _parse_background_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + background_color = _parse_background_color(d.pop("background_color", UNSET)) + + def _parse_font_family(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + font_family = _parse_font_family(d.pop("font_family", UNSET)) + + def _parse_style(data: object) -> None | TextStyle | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + style_type_1 = TextStyle(data) + + return style_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | TextStyle | Unset, data) + + style = _parse_style(d.pop("style", UNSET)) + + def _parse_align(data: object) -> HorizontalAlign | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + align_type_1 = HorizontalAlign(data) + + return align_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(HorizontalAlign | None | Unset, data) + + align = _parse_align(d.pop("align", UNSET)) + + def _parse_wrap(data: object) -> None | TextWrapMode | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + wrap_type_1 = TextWrapMode(data) + + return wrap_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | TextWrapMode | Unset, data) + + wrap = _parse_wrap(d.pop("wrap", UNSET)) + + def _parse_weight(data: object) -> None | TextWeight | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + weight_type_1 = TextWeight(data) + + return weight_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | TextWeight | Unset, data) + + weight = _parse_weight(d.pop("weight", UNSET)) + + text = cls( + text=text, + font_size=font_size, + type_=type_, + id=id, + width=width, + height=height, + max_width=max_width, + max_height=max_height, + line_height=line_height, + color=color, + background_color=background_color, + font_family=font_family, + style=style, + align=align, + wrap=wrap, + weight=weight, + ) + + return text diff --git a/fishjam/_composition_client/models/text_style.py b/fishjam/_composition_client/models/text_style.py new file mode 100644 index 0000000..c601cd9 --- /dev/null +++ b/fishjam/_composition_client/models/text_style.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class TextStyle(str, Enum): + """None""" + + ITALIC = "italic" + NORMAL = "normal" + OBLIQUE = "oblique" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/text_type.py b/fishjam/_composition_client/models/text_type.py new file mode 100644 index 0000000..9f9de15 --- /dev/null +++ b/fishjam/_composition_client/models/text_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class TextType(str, Enum): + """None""" + + TEXT = "text" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/text_weight.py b/fishjam/_composition_client/models/text_weight.py new file mode 100644 index 0000000..4d1cc01 --- /dev/null +++ b/fishjam/_composition_client/models/text_weight.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class TextWeight(str, Enum): + """Font weight, based on the [OpenType specification](https://learn.microsoft.com/en-gb/typography/opentype/spec/os2#usweightclass).""" + + BLACK = "black" + BOLD = "bold" + EXTRA_BOLD = "extra_bold" + EXTRA_LIGHT = "extra_light" + LIGHT = "light" + MEDIUM = "medium" + NORMAL = "normal" + SEMI_BOLD = "semi_bold" + THIN = "thin" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/text_wrap_mode.py b/fishjam/_composition_client/models/text_wrap_mode.py new file mode 100644 index 0000000..febaf4f --- /dev/null +++ b/fishjam/_composition_client/models/text_wrap_mode.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class TextWrapMode(str, Enum): + """None""" + + GLYPH = "glyph" + NONE = "none" + WORD = "word" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/tiles.py b/fishjam/_composition_client/models/tiles.py new file mode 100644 index 0000000..bc22d02 --- /dev/null +++ b/fishjam/_composition_client/models/tiles.py @@ -0,0 +1,425 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.horizontal_align import HorizontalAlign +from ..models.tiles_type import TilesType +from ..models.vertical_align import VerticalAlign +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.transition import Transition + from ..models.view import View + + +T = TypeVar("T", bound="Tiles") + + +@_attrs_define +class Tiles: + """ + Attributes: + type_ (TilesType): + id (None | str | Unset): + children (list[Image | InputStream | Rescaler | Text | Tiles | View] | None | Unset): List of component's + children. + width (float | None | Unset): Width of a component in pixels. Exact behavior might be different based on the + parent + component: + - If the parent component is a layout, check sections "Absolute positioning" and "Static + positioning" of that component. + - If the parent component is not a layout, then this field is required. + height (float | None | Unset): Height of a component in pixels. Exact behavior might be different based on the + parent + component: + - If the parent component is a layout, check sections "Absolute positioning" and "Static + positioning" of that component. + - If the parent component is not a layout, then this field is required. + background_color (None | str | Unset): + tile_aspect_ratio (None | str | Unset): + margin (float | None | Unset): (**default=`0`**) Margin of each tile in pixels. + padding (float | None | Unset): (**default=`0`**) Padding on each tile in pixels. + horizontal_align (HorizontalAlign | None | Unset): + vertical_align (None | Unset | VerticalAlign): + transition (None | Transition | Unset): + """ + + type_: TilesType + id: None | str | Unset = UNSET + children: ( + list[Image | InputStream | Rescaler | Text | Tiles | View] | None | Unset + ) = UNSET + width: float | None | Unset = UNSET + height: float | None | Unset = UNSET + background_color: None | str | Unset = UNSET + tile_aspect_ratio: None | str | Unset = UNSET + margin: float | None | Unset = UNSET + padding: float | None | Unset = UNSET + horizontal_align: HorizontalAlign | None | Unset = UNSET + vertical_align: None | Unset | VerticalAlign = UNSET + transition: None | Transition | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.transition import Transition + from ..models.view import View + + type_ = self.type_.value + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + children: list[dict[str, Any]] | None | Unset + if isinstance(self.children, Unset): + children = UNSET + elif isinstance(self.children, list): + children = [] + for children_type_0_item_data in self.children: + children_type_0_item: dict[str, Any] + if isinstance(children_type_0_item_data, InputStream): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, View): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, Text): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, Tiles): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, Rescaler): + children_type_0_item = children_type_0_item_data.to_dict() + else: + children_type_0_item = children_type_0_item_data.to_dict() + + children.append(children_type_0_item) + + else: + children = self.children + + width: float | None | Unset + if isinstance(self.width, Unset): + width = UNSET + else: + width = self.width + + height: float | None | Unset + if isinstance(self.height, Unset): + height = UNSET + else: + height = self.height + + background_color: None | str | Unset + if isinstance(self.background_color, Unset): + background_color = UNSET + else: + background_color = self.background_color + + tile_aspect_ratio: None | str | Unset + if isinstance(self.tile_aspect_ratio, Unset): + tile_aspect_ratio = UNSET + else: + tile_aspect_ratio = self.tile_aspect_ratio + + margin: float | None | Unset + if isinstance(self.margin, Unset): + margin = UNSET + else: + margin = self.margin + + padding: float | None | Unset + if isinstance(self.padding, Unset): + padding = UNSET + else: + padding = self.padding + + horizontal_align: None | str | Unset + if isinstance(self.horizontal_align, Unset): + horizontal_align = UNSET + elif isinstance(self.horizontal_align, HorizontalAlign): + horizontal_align = self.horizontal_align.value + else: + horizontal_align = self.horizontal_align + + vertical_align: None | str | Unset + if isinstance(self.vertical_align, Unset): + vertical_align = UNSET + elif isinstance(self.vertical_align, VerticalAlign): + vertical_align = self.vertical_align.value + else: + vertical_align = self.vertical_align + + transition: dict[str, Any] | None | Unset + if isinstance(self.transition, Unset): + transition = UNSET + elif isinstance(self.transition, Transition): + transition = self.transition.to_dict() + else: + transition = self.transition + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "type": type_, + }) + if id is not UNSET: + field_dict["id"] = id + if children is not UNSET: + field_dict["children"] = children + if width is not UNSET: + field_dict["width"] = width + if height is not UNSET: + field_dict["height"] = height + if background_color is not UNSET: + field_dict["background_color"] = background_color + if tile_aspect_ratio is not UNSET: + field_dict["tile_aspect_ratio"] = tile_aspect_ratio + if margin is not UNSET: + field_dict["margin"] = margin + if padding is not UNSET: + field_dict["padding"] = padding + if horizontal_align is not UNSET: + field_dict["horizontal_align"] = horizontal_align + if vertical_align is not UNSET: + field_dict["vertical_align"] = vertical_align + if transition is not UNSET: + field_dict["transition"] = transition + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.transition import Transition + from ..models.view import View + + d = dict(src_dict) + type_ = TilesType(d.pop("type")) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_children( + data: object, + ) -> list[Image | InputStream | Rescaler | Text | Tiles | View] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + children_type_0 = [] + _children_type_0 = data + for children_type_0_item_data in _children_type_0: + + def _parse_children_type_0_item( + data: object, + ) -> Image | InputStream | Rescaler | Text | Tiles | View: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_0 = InputStream.from_dict( + data + ) + + return componentsschemas_component_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_1 = View.from_dict(data) + + return componentsschemas_component_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_2 = Text.from_dict(data) + + return componentsschemas_component_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_3 = Tiles.from_dict(data) + + return componentsschemas_component_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_4 = Rescaler.from_dict( + data + ) + + return componentsschemas_component_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_5 = Image.from_dict(data) + + return componentsschemas_component_type_5 + + children_type_0_item = _parse_children_type_0_item( + children_type_0_item_data + ) + + children_type_0.append(children_type_0_item) + + return children_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + list[Image | InputStream | Rescaler | Text | Tiles | View] + | None + | Unset, + data, + ) + + children = _parse_children(d.pop("children", UNSET)) + + def _parse_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + width = _parse_width(d.pop("width", UNSET)) + + def _parse_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + height = _parse_height(d.pop("height", UNSET)) + + def _parse_background_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + background_color = _parse_background_color(d.pop("background_color", UNSET)) + + def _parse_tile_aspect_ratio(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + tile_aspect_ratio = _parse_tile_aspect_ratio(d.pop("tile_aspect_ratio", UNSET)) + + def _parse_margin(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + margin = _parse_margin(d.pop("margin", UNSET)) + + def _parse_padding(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding = _parse_padding(d.pop("padding", UNSET)) + + def _parse_horizontal_align(data: object) -> HorizontalAlign | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + horizontal_align_type_1 = HorizontalAlign(data) + + return horizontal_align_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(HorizontalAlign | None | Unset, data) + + horizontal_align = _parse_horizontal_align(d.pop("horizontal_align", UNSET)) + + def _parse_vertical_align(data: object) -> None | Unset | VerticalAlign: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + vertical_align_type_1 = VerticalAlign(data) + + return vertical_align_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | VerticalAlign, data) + + vertical_align = _parse_vertical_align(d.pop("vertical_align", UNSET)) + + def _parse_transition(data: object) -> None | Transition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + transition_type_1 = Transition.from_dict(data) + + return transition_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Transition | Unset, data) + + transition = _parse_transition(d.pop("transition", UNSET)) + + tiles = cls( + type_=type_, + id=id, + children=children, + width=width, + height=height, + background_color=background_color, + tile_aspect_ratio=tile_aspect_ratio, + margin=margin, + padding=padding, + horizontal_align=horizontal_align, + vertical_align=vertical_align, + transition=transition, + ) + + return tiles diff --git a/fishjam/_composition_client/models/tiles_type.py b/fishjam/_composition_client/models/tiles_type.py new file mode 100644 index 0000000..e651122 --- /dev/null +++ b/fishjam/_composition_client/models/tiles_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class TilesType(str, Enum): + """None""" + + TILES = "tiles" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/transition.py b/fishjam/_composition_client/models/transition.py new file mode 100644 index 0000000..20b1265 --- /dev/null +++ b/fishjam/_composition_client/models/transition.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.easing_function_bounce import EasingFunctionBounce + from ..models.easing_function_cubic_bezier import EasingFunctionCubicBezier + from ..models.easing_function_linear import EasingFunctionLinear + + +T = TypeVar("T", bound="Transition") + + +@_attrs_define +class Transition: + """ + Attributes: + duration_ms (float): Duration of a transition in milliseconds. + easing_function (EasingFunctionBounce | EasingFunctionCubicBezier | EasingFunctionLinear | None | Unset): + should_interrupt (bool | None | Unset): (**default=`false`**) On scene update, if there is already a transition + in progress, + it will be interrupted and the new transition will start from the current state. + """ + + duration_ms: float + easing_function: ( + EasingFunctionBounce + | EasingFunctionCubicBezier + | EasingFunctionLinear + | None + | Unset + ) = UNSET + should_interrupt: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.easing_function_bounce import EasingFunctionBounce + from ..models.easing_function_cubic_bezier import EasingFunctionCubicBezier + from ..models.easing_function_linear import EasingFunctionLinear + + duration_ms = self.duration_ms + + easing_function: dict[str, Any] | None | Unset + if isinstance(self.easing_function, Unset): + easing_function = UNSET + elif isinstance(self.easing_function, EasingFunctionLinear): + easing_function = self.easing_function.to_dict() + elif isinstance(self.easing_function, EasingFunctionBounce): + easing_function = self.easing_function.to_dict() + elif isinstance(self.easing_function, EasingFunctionCubicBezier): + easing_function = self.easing_function.to_dict() + else: + easing_function = self.easing_function + + should_interrupt: bool | None | Unset + if isinstance(self.should_interrupt, Unset): + should_interrupt = UNSET + else: + should_interrupt = self.should_interrupt + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "duration_ms": duration_ms, + }) + if easing_function is not UNSET: + field_dict["easing_function"] = easing_function + if should_interrupt is not UNSET: + field_dict["should_interrupt"] = should_interrupt + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.easing_function_bounce import EasingFunctionBounce + from ..models.easing_function_cubic_bezier import EasingFunctionCubicBezier + from ..models.easing_function_linear import EasingFunctionLinear + + d = dict(src_dict) + duration_ms = d.pop("duration_ms") + + def _parse_easing_function( + data: object, + ) -> ( + EasingFunctionBounce + | EasingFunctionCubicBezier + | EasingFunctionLinear + | None + | Unset + ): + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_easing_function_easing_function_linear = ( + EasingFunctionLinear.from_dict(data) + ) + + return componentsschemas_easing_function_easing_function_linear + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_easing_function_easing_function_bounce = ( + EasingFunctionBounce.from_dict(data) + ) + + return componentsschemas_easing_function_easing_function_bounce + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_easing_function_easing_function_cubic_bezier = ( + EasingFunctionCubicBezier.from_dict(data) + ) + + return componentsschemas_easing_function_easing_function_cubic_bezier + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + EasingFunctionBounce + | EasingFunctionCubicBezier + | EasingFunctionLinear + | None + | Unset, + data, + ) + + easing_function = _parse_easing_function(d.pop("easing_function", UNSET)) + + def _parse_should_interrupt(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + should_interrupt = _parse_should_interrupt(d.pop("should_interrupt", UNSET)) + + transition = cls( + duration_ms=duration_ms, + easing_function=easing_function, + should_interrupt=should_interrupt, + ) + + transition.additional_properties = d + return transition + + @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/_composition_client/models/transport_protocol.py b/fishjam/_composition_client/models/transport_protocol.py new file mode 100644 index 0000000..4007b0f --- /dev/null +++ b/fishjam/_composition_client/models/transport_protocol.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class TransportProtocol(str, Enum): + """None""" + + TCP_SERVER = "tcp_server" + UDP = "udp" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/unregister_input.py b/fishjam/_composition_client/models/unregister_input.py new file mode 100644 index 0000000..9854fd1 --- /dev/null +++ b/fishjam/_composition_client/models/unregister_input.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UnregisterInput") + + +@_attrs_define +class UnregisterInput: + """ + Attributes: + schedule_time_ms (float | None | Unset): Time in milliseconds when this request should be applied. Value `0` + represents + time of the start request. + """ + + schedule_time_ms: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + schedule_time_ms: float | None | Unset + if isinstance(self.schedule_time_ms, Unset): + schedule_time_ms = UNSET + else: + schedule_time_ms = self.schedule_time_ms + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if schedule_time_ms is not UNSET: + field_dict["schedule_time_ms"] = schedule_time_ms + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_schedule_time_ms(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + schedule_time_ms = _parse_schedule_time_ms(d.pop("schedule_time_ms", UNSET)) + + unregister_input = cls( + schedule_time_ms=schedule_time_ms, + ) + + unregister_input.additional_properties = d + return unregister_input + + @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/_composition_client/models/unregister_output.py b/fishjam/_composition_client/models/unregister_output.py new file mode 100644 index 0000000..dcd7f7a --- /dev/null +++ b/fishjam/_composition_client/models/unregister_output.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UnregisterOutput") + + +@_attrs_define +class UnregisterOutput: + """ + Attributes: + schedule_time_ms (float | None | Unset): Time in milliseconds when this request should be applied. Value `0` + represents + time of the start request. + """ + + schedule_time_ms: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + schedule_time_ms: float | None | Unset + if isinstance(self.schedule_time_ms, Unset): + schedule_time_ms = UNSET + else: + schedule_time_ms = self.schedule_time_ms + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if schedule_time_ms is not UNSET: + field_dict["schedule_time_ms"] = schedule_time_ms + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_schedule_time_ms(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + schedule_time_ms = _parse_schedule_time_ms(d.pop("schedule_time_ms", UNSET)) + + unregister_output = cls( + schedule_time_ms=schedule_time_ms, + ) + + unregister_output.additional_properties = d + return unregister_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/_composition_client/models/unregister_renderer.py b/fishjam/_composition_client/models/unregister_renderer.py new file mode 100644 index 0000000..b32b9d3 --- /dev/null +++ b/fishjam/_composition_client/models/unregister_renderer.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="UnregisterRenderer") + + +@_attrs_define +class UnregisterRenderer: + """ + Attributes: + schedule_time_ms (float | None | Unset): Time in milliseconds when this request should be applied. Value `0` + represents + time of the start request. + """ + + schedule_time_ms: float | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + schedule_time_ms: float | None | Unset + if isinstance(self.schedule_time_ms, Unset): + schedule_time_ms = UNSET + else: + schedule_time_ms = self.schedule_time_ms + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if schedule_time_ms is not UNSET: + field_dict["schedule_time_ms"] = schedule_time_ms + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_schedule_time_ms(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + schedule_time_ms = _parse_schedule_time_ms(d.pop("schedule_time_ms", UNSET)) + + unregister_renderer = cls( + schedule_time_ms=schedule_time_ms, + ) + + unregister_renderer.additional_properties = d + return unregister_renderer + + @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/_composition_client/models/update_output_request.py b/fishjam/_composition_client/models/update_output_request.py new file mode 100644 index 0000000..5d65a22 --- /dev/null +++ b/fishjam/_composition_client/models/update_output_request.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.audio_scene import AudioScene + from ..models.video_scene import VideoScene + + +T = TypeVar("T", bound="UpdateOutputRequest") + + +@_attrs_define +class UpdateOutputRequest: + """ + Attributes: + video (None | Unset | VideoScene): + audio (AudioScene | None | Unset): + schedule_time_ms (float | None | Unset): + """ + + video: None | Unset | VideoScene = UNSET + audio: AudioScene | None | Unset = UNSET + schedule_time_ms: float | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.audio_scene import AudioScene + from ..models.video_scene import VideoScene + + video: dict[str, Any] | None | Unset + if isinstance(self.video, Unset): + video = UNSET + elif isinstance(self.video, VideoScene): + video = self.video.to_dict() + else: + video = self.video + + audio: dict[str, Any] | None | Unset + if isinstance(self.audio, Unset): + audio = UNSET + elif isinstance(self.audio, AudioScene): + audio = self.audio.to_dict() + else: + audio = self.audio + + schedule_time_ms: float | None | Unset + if isinstance(self.schedule_time_ms, Unset): + schedule_time_ms = UNSET + else: + schedule_time_ms = self.schedule_time_ms + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if video is not UNSET: + field_dict["video"] = video + if audio is not UNSET: + field_dict["audio"] = audio + if schedule_time_ms is not UNSET: + field_dict["schedule_time_ms"] = schedule_time_ms + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.audio_scene import AudioScene + from ..models.video_scene import VideoScene + + d = dict(src_dict) + + def _parse_video(data: object) -> None | Unset | VideoScene: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + video_type_1 = VideoScene.from_dict(data) + + return video_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | VideoScene, data) + + video = _parse_video(d.pop("video", UNSET)) + + def _parse_audio(data: object) -> AudioScene | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + audio_type_1 = AudioScene.from_dict(data) + + return audio_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AudioScene | None | Unset, data) + + audio = _parse_audio(d.pop("audio", UNSET)) + + def _parse_schedule_time_ms(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + schedule_time_ms = _parse_schedule_time_ms(d.pop("schedule_time_ms", UNSET)) + + update_output_request = cls( + video=video, + audio=audio, + schedule_time_ms=schedule_time_ms, + ) + + return update_output_request diff --git a/fishjam/_composition_client/models/vertical_align.py b/fishjam/_composition_client/models/vertical_align.py new file mode 100644 index 0000000..70054d6 --- /dev/null +++ b/fishjam/_composition_client/models/vertical_align.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class VerticalAlign(str, Enum): + """None""" + + BOTTOM = "bottom" + CENTER = "center" + JUSTIFIED = "justified" + TOP = "top" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/video_scene.py b/fishjam/_composition_client/models/video_scene.py new file mode 100644 index 0000000..3c5215c --- /dev/null +++ b/fishjam/_composition_client/models/video_scene.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.view import View + + +T = TypeVar("T", bound="VideoScene") + + +@_attrs_define +class VideoScene: + """ + Attributes: + root (Image | InputStream | Rescaler | Text | Tiles | View): + """ + + root: Image | InputStream | Rescaler | Text | Tiles | View + + def to_dict(self) -> dict[str, Any]: + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.view import View + + root: dict[str, Any] + if isinstance(self.root, InputStream): + root = self.root.to_dict() + elif isinstance(self.root, View): + root = self.root.to_dict() + elif isinstance(self.root, Text): + root = self.root.to_dict() + elif isinstance(self.root, Tiles): + root = self.root.to_dict() + elif isinstance(self.root, Rescaler): + root = self.root.to_dict() + else: + root = self.root.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "root": root, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.view import View + + d = dict(src_dict) + + def _parse_root( + data: object, + ) -> Image | InputStream | Rescaler | Text | Tiles | View: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_0 = InputStream.from_dict(data) + + return componentsschemas_component_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_1 = View.from_dict(data) + + return componentsschemas_component_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_2 = Text.from_dict(data) + + return componentsschemas_component_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_3 = Tiles.from_dict(data) + + return componentsschemas_component_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_4 = Rescaler.from_dict(data) + + return componentsschemas_component_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_5 = Image.from_dict(data) + + return componentsschemas_component_type_5 + + root = _parse_root(d.pop("root")) + + video_scene = cls( + root=root, + ) + + return video_scene diff --git a/fishjam/_composition_client/models/view.py b/fishjam/_composition_client/models/view.py new file mode 100644 index 0000000..b6e791d --- /dev/null +++ b/fishjam/_composition_client/models/view.py @@ -0,0 +1,723 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.overflow import Overflow +from ..models.view_direction import ViewDirection +from ..models.view_type import ViewType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.box_shadow import BoxShadow + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.transition import Transition + + +T = TypeVar("T", bound="View") + + +@_attrs_define +class View: + """ + Attributes: + type_ (ViewType): + id (None | str | Unset): + children (list[Image | InputStream | Rescaler | Text | Tiles | View] | None | Unset): List of component's + children. + width (float | None | Unset): Width of a component in pixels (without a border). Exact behavior might be + different + based on the parent component: + - If the parent component is a layout, check sections "Absolute positioning" and "Static + positioning" of that component. + - If the parent component is not a layout, then this field is required. + height (float | None | Unset): Height of a component in pixels (without a border). Exact behavior might be + different + based on the parent component: + - If the parent component is a layout, check sections "Absolute positioning" and "Static + positioning" of that component. + - If the parent component is not a layout, then this field is required. + direction (None | Unset | ViewDirection): + top (float | None | Unset): Distance in pixels between this component's top edge and its parent's top edge + (including a border). + If this field is defined, then the component will ignore a layout defined by its parent. + left (float | None | Unset): Distance in pixels between this component's left edge and its parent's left edge + (including a border). + If this field is defined, this element will be absolutely positioned, instead of being + laid out by its parent. + bottom (float | None | Unset): Distance in pixels between the bottom edge of this component and the bottom edge + of its + parent (including a border). If this field is defined, this element will be absolutely + positioned, instead of being laid out by its parent. + right (float | None | Unset): Distance in pixels between this component's right edge and its parent's right + edge. + If this field is defined, this element will be absolutely positioned, instead of being + laid out by its parent. + rotation (float | None | Unset): Rotation of a component in degrees. If this field is defined, this element will + be + absolutely positioned, instead of being laid out by its parent. + transition (None | Transition | Unset): + overflow (None | Overflow | Unset): + background_color (None | str | Unset): + border_radius (float | None | Unset): (**default=`0.0`**) Radius of a rounded corner. + border_width (float | None | Unset): (**default=`0.0`**) Border width. + border_color (None | str | Unset): + box_shadow (list[BoxShadow] | None | Unset): List of box shadows. + padding (float | None | Unset): (**default=`0.0`**) Padding for all sides of the component. + padding_vertical (float | None | Unset): (**default=`0.0`**) Padding for the top and bottom of the component. + padding_horizontal (float | None | Unset): (**default=`0.0`**) Padding for the left and right of the component. + padding_top (float | None | Unset): (**default=`0.0`**) Padding on top side in pixels. + padding_right (float | None | Unset): (**default=`0.0`**) Padding on right side in pixels. + padding_bottom (float | None | Unset): (**default=`0.0`**) Padding on bottom side in pixels. + padding_left (float | None | Unset): (**default=`0.0`**) Padding on left side in pixels. + """ + + type_: ViewType + id: None | str | Unset = UNSET + children: ( + list[Image | InputStream | Rescaler | Text | Tiles | View] | None | Unset + ) = UNSET + width: float | None | Unset = UNSET + height: float | None | Unset = UNSET + direction: None | Unset | ViewDirection = UNSET + top: float | None | Unset = UNSET + left: float | None | Unset = UNSET + bottom: float | None | Unset = UNSET + right: float | None | Unset = UNSET + rotation: float | None | Unset = UNSET + transition: None | Transition | Unset = UNSET + overflow: None | Overflow | Unset = UNSET + background_color: None | str | Unset = UNSET + border_radius: float | None | Unset = UNSET + border_width: float | None | Unset = UNSET + border_color: None | str | Unset = UNSET + box_shadow: list[BoxShadow] | None | Unset = UNSET + padding: float | None | Unset = UNSET + padding_vertical: float | None | Unset = UNSET + padding_horizontal: float | None | Unset = UNSET + padding_top: float | None | Unset = UNSET + padding_right: float | None | Unset = UNSET + padding_bottom: float | None | Unset = UNSET + padding_left: float | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.transition import Transition + + type_ = self.type_.value + + id: None | str | Unset + if isinstance(self.id, Unset): + id = UNSET + else: + id = self.id + + children: list[dict[str, Any]] | None | Unset + if isinstance(self.children, Unset): + children = UNSET + elif isinstance(self.children, list): + children = [] + for children_type_0_item_data in self.children: + children_type_0_item: dict[str, Any] + if isinstance(children_type_0_item_data, InputStream): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, View): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, Text): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, Tiles): + children_type_0_item = children_type_0_item_data.to_dict() + elif isinstance(children_type_0_item_data, Rescaler): + children_type_0_item = children_type_0_item_data.to_dict() + else: + children_type_0_item = children_type_0_item_data.to_dict() + + children.append(children_type_0_item) + + else: + children = self.children + + width: float | None | Unset + if isinstance(self.width, Unset): + width = UNSET + else: + width = self.width + + height: float | None | Unset + if isinstance(self.height, Unset): + height = UNSET + else: + height = self.height + + direction: None | str | Unset + if isinstance(self.direction, Unset): + direction = UNSET + elif isinstance(self.direction, ViewDirection): + direction = self.direction.value + else: + direction = self.direction + + top: float | None | Unset + if isinstance(self.top, Unset): + top = UNSET + else: + top = self.top + + left: float | None | Unset + if isinstance(self.left, Unset): + left = UNSET + else: + left = self.left + + bottom: float | None | Unset + if isinstance(self.bottom, Unset): + bottom = UNSET + else: + bottom = self.bottom + + right: float | None | Unset + if isinstance(self.right, Unset): + right = UNSET + else: + right = self.right + + rotation: float | None | Unset + if isinstance(self.rotation, Unset): + rotation = UNSET + else: + rotation = self.rotation + + transition: dict[str, Any] | None | Unset + if isinstance(self.transition, Unset): + transition = UNSET + elif isinstance(self.transition, Transition): + transition = self.transition.to_dict() + else: + transition = self.transition + + overflow: None | str | Unset + if isinstance(self.overflow, Unset): + overflow = UNSET + elif isinstance(self.overflow, Overflow): + overflow = self.overflow.value + else: + overflow = self.overflow + + background_color: None | str | Unset + if isinstance(self.background_color, Unset): + background_color = UNSET + else: + background_color = self.background_color + + border_radius: float | None | Unset + if isinstance(self.border_radius, Unset): + border_radius = UNSET + else: + border_radius = self.border_radius + + border_width: float | None | Unset + if isinstance(self.border_width, Unset): + border_width = UNSET + else: + border_width = self.border_width + + border_color: None | str | Unset + if isinstance(self.border_color, Unset): + border_color = UNSET + else: + border_color = self.border_color + + box_shadow: list[dict[str, Any]] | None | Unset + if isinstance(self.box_shadow, Unset): + box_shadow = UNSET + elif isinstance(self.box_shadow, list): + box_shadow = [] + for box_shadow_type_0_item_data in self.box_shadow: + box_shadow_type_0_item = box_shadow_type_0_item_data.to_dict() + box_shadow.append(box_shadow_type_0_item) + + else: + box_shadow = self.box_shadow + + padding: float | None | Unset + if isinstance(self.padding, Unset): + padding = UNSET + else: + padding = self.padding + + padding_vertical: float | None | Unset + if isinstance(self.padding_vertical, Unset): + padding_vertical = UNSET + else: + padding_vertical = self.padding_vertical + + padding_horizontal: float | None | Unset + if isinstance(self.padding_horizontal, Unset): + padding_horizontal = UNSET + else: + padding_horizontal = self.padding_horizontal + + padding_top: float | None | Unset + if isinstance(self.padding_top, Unset): + padding_top = UNSET + else: + padding_top = self.padding_top + + padding_right: float | None | Unset + if isinstance(self.padding_right, Unset): + padding_right = UNSET + else: + padding_right = self.padding_right + + padding_bottom: float | None | Unset + if isinstance(self.padding_bottom, Unset): + padding_bottom = UNSET + else: + padding_bottom = self.padding_bottom + + padding_left: float | None | Unset + if isinstance(self.padding_left, Unset): + padding_left = UNSET + else: + padding_left = self.padding_left + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "type": type_, + }) + if id is not UNSET: + field_dict["id"] = id + if children is not UNSET: + field_dict["children"] = children + if width is not UNSET: + field_dict["width"] = width + if height is not UNSET: + field_dict["height"] = height + if direction is not UNSET: + field_dict["direction"] = direction + if top is not UNSET: + field_dict["top"] = top + if left is not UNSET: + field_dict["left"] = left + if bottom is not UNSET: + field_dict["bottom"] = bottom + if right is not UNSET: + field_dict["right"] = right + if rotation is not UNSET: + field_dict["rotation"] = rotation + if transition is not UNSET: + field_dict["transition"] = transition + if overflow is not UNSET: + field_dict["overflow"] = overflow + if background_color is not UNSET: + field_dict["background_color"] = background_color + if border_radius is not UNSET: + field_dict["border_radius"] = border_radius + if border_width is not UNSET: + field_dict["border_width"] = border_width + if border_color is not UNSET: + field_dict["border_color"] = border_color + if box_shadow is not UNSET: + field_dict["box_shadow"] = box_shadow + if padding is not UNSET: + field_dict["padding"] = padding + if padding_vertical is not UNSET: + field_dict["padding_vertical"] = padding_vertical + if padding_horizontal is not UNSET: + field_dict["padding_horizontal"] = padding_horizontal + if padding_top is not UNSET: + field_dict["padding_top"] = padding_top + if padding_right is not UNSET: + field_dict["padding_right"] = padding_right + if padding_bottom is not UNSET: + field_dict["padding_bottom"] = padding_bottom + if padding_left is not UNSET: + field_dict["padding_left"] = padding_left + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.box_shadow import BoxShadow + from ..models.image import Image + from ..models.input_stream import InputStream + from ..models.rescaler import Rescaler + from ..models.text import Text + from ..models.tiles import Tiles + from ..models.transition import Transition + + d = dict(src_dict) + type_ = ViewType(d.pop("type")) + + def _parse_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + id = _parse_id(d.pop("id", UNSET)) + + def _parse_children( + data: object, + ) -> list[Image | InputStream | Rescaler | Text | Tiles | View] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + children_type_0 = [] + _children_type_0 = data + for children_type_0_item_data in _children_type_0: + + def _parse_children_type_0_item( + data: object, + ) -> Image | InputStream | Rescaler | Text | Tiles | View: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_0 = InputStream.from_dict( + data + ) + + return componentsschemas_component_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_1 = View.from_dict(data) + + return componentsschemas_component_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_2 = Text.from_dict(data) + + return componentsschemas_component_type_2 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_3 = Tiles.from_dict(data) + + return componentsschemas_component_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_4 = Rescaler.from_dict( + data + ) + + return componentsschemas_component_type_4 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_component_type_5 = Image.from_dict(data) + + return componentsschemas_component_type_5 + + children_type_0_item = _parse_children_type_0_item( + children_type_0_item_data + ) + + children_type_0.append(children_type_0_item) + + return children_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast( + list[Image | InputStream | Rescaler | Text | Tiles | View] + | None + | Unset, + data, + ) + + children = _parse_children(d.pop("children", UNSET)) + + def _parse_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + width = _parse_width(d.pop("width", UNSET)) + + def _parse_height(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + height = _parse_height(d.pop("height", UNSET)) + + def _parse_direction(data: object) -> None | Unset | ViewDirection: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + direction_type_1 = ViewDirection(data) + + return direction_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | ViewDirection, data) + + direction = _parse_direction(d.pop("direction", UNSET)) + + def _parse_top(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + top = _parse_top(d.pop("top", UNSET)) + + def _parse_left(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + left = _parse_left(d.pop("left", UNSET)) + + def _parse_bottom(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + bottom = _parse_bottom(d.pop("bottom", UNSET)) + + def _parse_right(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + right = _parse_right(d.pop("right", UNSET)) + + def _parse_rotation(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + rotation = _parse_rotation(d.pop("rotation", UNSET)) + + def _parse_transition(data: object) -> None | Transition | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + transition_type_1 = Transition.from_dict(data) + + return transition_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Transition | Unset, data) + + transition = _parse_transition(d.pop("transition", UNSET)) + + def _parse_overflow(data: object) -> None | Overflow | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + overflow_type_1 = Overflow(data) + + return overflow_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Overflow | Unset, data) + + overflow = _parse_overflow(d.pop("overflow", UNSET)) + + def _parse_background_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + background_color = _parse_background_color(d.pop("background_color", UNSET)) + + def _parse_border_radius(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + border_radius = _parse_border_radius(d.pop("border_radius", UNSET)) + + def _parse_border_width(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + border_width = _parse_border_width(d.pop("border_width", UNSET)) + + def _parse_border_color(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + border_color = _parse_border_color(d.pop("border_color", UNSET)) + + def _parse_box_shadow(data: object) -> list[BoxShadow] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + box_shadow_type_0 = [] + _box_shadow_type_0 = data + for box_shadow_type_0_item_data in _box_shadow_type_0: + box_shadow_type_0_item = BoxShadow.from_dict( + box_shadow_type_0_item_data + ) + + box_shadow_type_0.append(box_shadow_type_0_item) + + return box_shadow_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[BoxShadow] | None | Unset, data) + + box_shadow = _parse_box_shadow(d.pop("box_shadow", UNSET)) + + def _parse_padding(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding = _parse_padding(d.pop("padding", UNSET)) + + def _parse_padding_vertical(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding_vertical = _parse_padding_vertical(d.pop("padding_vertical", UNSET)) + + def _parse_padding_horizontal(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding_horizontal = _parse_padding_horizontal( + d.pop("padding_horizontal", UNSET) + ) + + def _parse_padding_top(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding_top = _parse_padding_top(d.pop("padding_top", UNSET)) + + def _parse_padding_right(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding_right = _parse_padding_right(d.pop("padding_right", UNSET)) + + def _parse_padding_bottom(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding_bottom = _parse_padding_bottom(d.pop("padding_bottom", UNSET)) + + def _parse_padding_left(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + padding_left = _parse_padding_left(d.pop("padding_left", UNSET)) + + view = cls( + type_=type_, + id=id, + children=children, + width=width, + height=height, + direction=direction, + top=top, + left=left, + bottom=bottom, + right=right, + rotation=rotation, + transition=transition, + overflow=overflow, + background_color=background_color, + border_radius=border_radius, + border_width=border_width, + border_color=border_color, + box_shadow=box_shadow, + padding=padding, + padding_vertical=padding_vertical, + padding_horizontal=padding_horizontal, + padding_top=padding_top, + padding_right=padding_right, + padding_bottom=padding_bottom, + padding_left=padding_left, + ) + + return view diff --git a/fishjam/_composition_client/models/view_direction.py b/fishjam/_composition_client/models/view_direction.py new file mode 100644 index 0000000..4357992 --- /dev/null +++ b/fishjam/_composition_client/models/view_direction.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class ViewDirection(str, Enum): + """None""" + + COLUMN = "column" + ROW = "row" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/view_type.py b/fishjam/_composition_client/models/view_type.py new file mode 100644 index 0000000..29b52ab --- /dev/null +++ b/fishjam/_composition_client/models/view_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ViewType(str, Enum): + """None""" + + VIEW = "view" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/whep_input.py b/fishjam/_composition_client/models/whep_input.py new file mode 100644 index 0000000..d5d3934 --- /dev/null +++ b/fishjam/_composition_client/models/whep_input.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.whep_input_type import WhepInputType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="WhepInput") + + +@_attrs_define +class WhepInput: + """ + Attributes: + endpoint_url (str): WHEP server endpoint URL + type_ (WhepInputType): + bearer_token (None | str | Unset): Optional Bearer token for auth + video (bool | None | Unset): If `true`, requests a h264-encoded video track. + If not provided, it defaults to `true` + """ + + endpoint_url: str + type_: WhepInputType + bearer_token: None | str | Unset = UNSET + video: bool | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + endpoint_url = self.endpoint_url + + type_ = self.type_.value + + bearer_token: None | str | Unset + if isinstance(self.bearer_token, Unset): + bearer_token = UNSET + else: + bearer_token = self.bearer_token + + video: bool | None | Unset + if isinstance(self.video, Unset): + video = UNSET + else: + video = self.video + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "endpoint_url": endpoint_url, + "type": type_, + }) + if bearer_token is not UNSET: + field_dict["bearer_token"] = bearer_token + if video is not UNSET: + field_dict["video"] = video + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + endpoint_url = d.pop("endpoint_url") + + type_ = WhepInputType(d.pop("type")) + + def _parse_bearer_token(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + bearer_token = _parse_bearer_token(d.pop("bearer_token", UNSET)) + + def _parse_video(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + video = _parse_video(d.pop("video", UNSET)) + + whep_input = cls( + endpoint_url=endpoint_url, + type_=type_, + bearer_token=bearer_token, + video=video, + ) + + return whep_input diff --git a/fishjam/_composition_client/models/whep_input_type.py b/fishjam/_composition_client/models/whep_input_type.py new file mode 100644 index 0000000..68520dd --- /dev/null +++ b/fishjam/_composition_client/models/whep_input_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class WhepInputType(str, Enum): + """None""" + + WHEP_CLIENT = "whep_client" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/whip_audio_encoder_options_any.py b/fishjam/_composition_client/models/whip_audio_encoder_options_any.py new file mode 100644 index 0000000..9fc2b3a --- /dev/null +++ b/fishjam/_composition_client/models/whip_audio_encoder_options_any.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.whip_audio_encoder_options_any_type import WhipAudioEncoderOptionsAnyType + +T = TypeVar("T", bound="WhipAudioEncoderOptionsAny") + + +@_attrs_define +class WhipAudioEncoderOptionsAny: + """ + Attributes: + type_ (WhipAudioEncoderOptionsAnyType): + """ + + type_: WhipAudioEncoderOptionsAnyType + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + 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({ + "type": type_, + }) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + type_ = WhipAudioEncoderOptionsAnyType(d.pop("type")) + + whip_audio_encoder_options_any = cls( + type_=type_, + ) + + whip_audio_encoder_options_any.additional_properties = d + return whip_audio_encoder_options_any + + @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/_composition_client/models/whip_audio_encoder_options_any_type.py b/fishjam/_composition_client/models/whip_audio_encoder_options_any_type.py new file mode 100644 index 0000000..c9c4057 --- /dev/null +++ b/fishjam/_composition_client/models/whip_audio_encoder_options_any_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class WhipAudioEncoderOptionsAnyType(str, Enum): + """None""" + + ANY = "any" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/whip_audio_encoder_options_opus.py b/fishjam/_composition_client/models/whip_audio_encoder_options_opus.py new file mode 100644 index 0000000..dcbad3e --- /dev/null +++ b/fishjam/_composition_client/models/whip_audio_encoder_options_opus.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.opus_encoder_preset import OpusEncoderPreset +from ..models.whip_audio_encoder_options_opus_type import ( + WhipAudioEncoderOptionsOpusType, +) +from ..types import UNSET, Unset + +T = TypeVar("T", bound="WhipAudioEncoderOptionsOpus") + + +@_attrs_define +class WhipAudioEncoderOptionsOpus: + """ + Attributes: + type_ (WhipAudioEncoderOptionsOpusType): + preset (None | OpusEncoderPreset | Unset): + sample_rate (int | None | Unset): (**default=`48000`**) Sample rate. Allowed values: [8000, 16000, 24000, + 48000]. + forward_error_correction (bool | None | Unset): (**default=`false`**) Specifies if forward error correction + (FEC) should be used. + """ + + type_: WhipAudioEncoderOptionsOpusType + preset: None | OpusEncoderPreset | Unset = UNSET + sample_rate: int | None | Unset = UNSET + forward_error_correction: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_.value + + preset: None | str | Unset + if isinstance(self.preset, Unset): + preset = UNSET + elif isinstance(self.preset, OpusEncoderPreset): + preset = self.preset.value + else: + preset = self.preset + + sample_rate: int | None | Unset + if isinstance(self.sample_rate, Unset): + sample_rate = UNSET + else: + sample_rate = self.sample_rate + + forward_error_correction: bool | None | Unset + if isinstance(self.forward_error_correction, Unset): + forward_error_correction = UNSET + else: + forward_error_correction = self.forward_error_correction + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "type": type_, + }) + if preset is not UNSET: + field_dict["preset"] = preset + if sample_rate is not UNSET: + field_dict["sample_rate"] = sample_rate + if forward_error_correction is not UNSET: + field_dict["forward_error_correction"] = forward_error_correction + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + type_ = WhipAudioEncoderOptionsOpusType(d.pop("type")) + + def _parse_preset(data: object) -> None | OpusEncoderPreset | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + preset_type_1 = OpusEncoderPreset(data) + + return preset_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OpusEncoderPreset | Unset, data) + + preset = _parse_preset(d.pop("preset", UNSET)) + + def _parse_sample_rate(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + sample_rate = _parse_sample_rate(d.pop("sample_rate", UNSET)) + + def _parse_forward_error_correction(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + forward_error_correction = _parse_forward_error_correction( + d.pop("forward_error_correction", UNSET) + ) + + whip_audio_encoder_options_opus = cls( + type_=type_, + preset=preset, + sample_rate=sample_rate, + forward_error_correction=forward_error_correction, + ) + + whip_audio_encoder_options_opus.additional_properties = d + return whip_audio_encoder_options_opus + + @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/_composition_client/models/whip_audio_encoder_options_opus_type.py b/fishjam/_composition_client/models/whip_audio_encoder_options_opus_type.py new file mode 100644 index 0000000..3229a23 --- /dev/null +++ b/fishjam/_composition_client/models/whip_audio_encoder_options_opus_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class WhipAudioEncoderOptionsOpusType(str, Enum): + """None""" + + OPUS = "opus" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/whip_input.py b/fishjam/_composition_client/models/whip_input.py new file mode 100644 index 0000000..11e76e2 --- /dev/null +++ b/fishjam/_composition_client/models/whip_input.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.whip_input_type import WhipInputType +from ..types import UNSET, Unset + +T = TypeVar("T", bound="WhipInput") + + +@_attrs_define +class WhipInput: + """ + Attributes: + type_ (WhipInputType): + bearer_token (None | str | Unset): Token used for authentication in WHIP protocol. If not provided, the random + value + will be generated and returned in the response. + video (bool | None | Unset): If `true`, accepts a h264-encoded video track. + If not provided, it defaults to `true` + """ + + type_: WhipInputType + bearer_token: None | str | Unset = UNSET + video: bool | None | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_.value + + bearer_token: None | str | Unset + if isinstance(self.bearer_token, Unset): + bearer_token = UNSET + else: + bearer_token = self.bearer_token + + video: bool | None | Unset + if isinstance(self.video, Unset): + video = UNSET + else: + video = self.video + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "type": type_, + }) + if bearer_token is not UNSET: + field_dict["bearer_token"] = bearer_token + if video is not UNSET: + field_dict["video"] = video + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + type_ = WhipInputType(d.pop("type")) + + def _parse_bearer_token(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + bearer_token = _parse_bearer_token(d.pop("bearer_token", UNSET)) + + def _parse_video(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + video = _parse_video(d.pop("video", UNSET)) + + whip_input = cls( + type_=type_, + bearer_token=bearer_token, + video=video, + ) + + return whip_input diff --git a/fishjam/_composition_client/models/whip_input_type.py b/fishjam/_composition_client/models/whip_input_type.py new file mode 100644 index 0000000..e812626 --- /dev/null +++ b/fishjam/_composition_client/models/whip_input_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class WhipInputType(str, Enum): + """None""" + + WHIP_SERVER = "whip_server" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/models/whip_output.py b/fishjam/_composition_client/models/whip_output.py new file mode 100644 index 0000000..cabcbe0 --- /dev/null +++ b/fishjam/_composition_client/models/whip_output.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define + +from ..models.whip_output_type import WhipOutputType +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.output_whip_audio_options import OutputWhipAudioOptions + from ..models.output_whip_video_options import OutputWhipVideoOptions + + +T = TypeVar("T", bound="WhipOutput") + + +@_attrs_define +class WhipOutput: + """ + Attributes: + endpoint_url (str): WHIP server endpoint + type_ (WhipOutputType): + bearer_token (None | str | Unset): + video (None | OutputWhipVideoOptions | Unset): + audio (None | OutputWhipAudioOptions | Unset): + """ + + endpoint_url: str + type_: WhipOutputType + bearer_token: None | str | Unset = UNSET + video: None | OutputWhipVideoOptions | Unset = UNSET + audio: None | OutputWhipAudioOptions | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.output_whip_audio_options import OutputWhipAudioOptions + from ..models.output_whip_video_options import OutputWhipVideoOptions + + endpoint_url = self.endpoint_url + + type_ = self.type_.value + + bearer_token: None | str | Unset + if isinstance(self.bearer_token, Unset): + bearer_token = UNSET + else: + bearer_token = self.bearer_token + + video: dict[str, Any] | None | Unset + if isinstance(self.video, Unset): + video = UNSET + elif isinstance(self.video, OutputWhipVideoOptions): + video = self.video.to_dict() + else: + video = self.video + + audio: dict[str, Any] | None | Unset + if isinstance(self.audio, Unset): + audio = UNSET + elif isinstance(self.audio, OutputWhipAudioOptions): + audio = self.audio.to_dict() + else: + audio = self.audio + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "endpoint_url": endpoint_url, + "type": type_, + }) + if bearer_token is not UNSET: + field_dict["bearer_token"] = bearer_token + if video is not UNSET: + field_dict["video"] = video + if audio is not UNSET: + field_dict["audio"] = audio + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.output_whip_audio_options import OutputWhipAudioOptions + from ..models.output_whip_video_options import OutputWhipVideoOptions + + d = dict(src_dict) + endpoint_url = d.pop("endpoint_url") + + type_ = WhipOutputType(d.pop("type")) + + def _parse_bearer_token(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + bearer_token = _parse_bearer_token(d.pop("bearer_token", UNSET)) + + def _parse_video(data: object) -> None | OutputWhipVideoOptions | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + video_type_1 = OutputWhipVideoOptions.from_dict(data) + + return video_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputWhipVideoOptions | Unset, data) + + video = _parse_video(d.pop("video", UNSET)) + + def _parse_audio(data: object) -> None | OutputWhipAudioOptions | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + audio_type_1 = OutputWhipAudioOptions.from_dict(data) + + return audio_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | OutputWhipAudioOptions | Unset, data) + + audio = _parse_audio(d.pop("audio", UNSET)) + + whip_output = cls( + endpoint_url=endpoint_url, + type_=type_, + bearer_token=bearer_token, + video=video, + audio=audio, + ) + + return whip_output diff --git a/fishjam/_composition_client/models/whip_output_type.py b/fishjam/_composition_client/models/whip_output_type.py new file mode 100644 index 0000000..e2278f6 --- /dev/null +++ b/fishjam/_composition_client/models/whip_output_type.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class WhipOutputType(str, Enum): + """None""" + + WHIP_CLIENT = "whip_client" + + def __str__(self) -> str: + return str(self.value) diff --git a/fishjam/_composition_client/types.py b/fishjam/_composition_client/types.py new file mode 100644 index 0000000..b64af09 --- /dev/null +++ b/fishjam/_composition_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/openapi-python-client-composition-config.yaml b/openapi-python-client-composition-config.yaml new file mode 100644 index 0000000..665c33c --- /dev/null +++ b/openapi-python-client-composition-config.yaml @@ -0,0 +1,2 @@ +project_name_override: fishjam +package_name_override: _composition_client diff --git a/pyproject.toml b/pyproject.toml index f8bffbf..7c173a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ fix_lint = "scripts:run_linter_fix" generate_docs = "scripts:generate_docs" generate_docusaurus = "scripts:generate_docusaurus" update_client = "scripts:update_client" +update_composition_client = "scripts:update_composition_client" room_manager = "scripts:start_room_manager" [project.optional-dependencies] @@ -89,6 +90,7 @@ convention = "google" [tool.ruff.lint.extend-per-file-ignores] "fishjam/_openapi_client/**" = ["E501", "D", "DOC"] +"fishjam/_composition_client/**" = ["E501", "D", "DOC"] "fishjam/events/_protos/**" = ["D", "DOC"] "fishjam/errors.py" = ["D415", "D419", "DOC501", "DOC201"] "**/__init__.py" = ["D415", "DOC"] @@ -109,6 +111,7 @@ exclude = [ ".ruff_cache", "lib", "fishjam/_openapi_client", + "fishjam/_composition_client", "tests", ] typeCheckingMode = "basic" diff --git a/scripts.py b/scripts.py index 8a23a98..a08dd66 100644 --- a/scripts.py +++ b/scripts.py @@ -137,25 +137,43 @@ def generate_docusaurus(): dest_path.write_text(safe_content, encoding="utf-8") -def update_client(): +def _spec_argument(name: str) -> str: if len(sys.argv) < 2: - raise RuntimeError("Missing fishjam openapi.yaml raw url positional argument") + raise RuntimeError(f"Missing {name} url or path positional argument") url_or_path = sys.argv[1] is_url = url_or_path.startswith("http://") or url_or_path.startswith("https://") - file_arg = f"--url {url_or_path}" if is_url else f"--path {url_or_path}" + return f"--url {url_or_path}" if is_url else f"--path {url_or_path}" + +def _generate_client(file_arg: str, config: str, output_path: str): check_exit_code( f"openapi-python-client generate \ {file_arg} \ - --config openapi-python-client-config.yaml \ + --config {config} \ --meta=none \ --overwrite \ - --output-path=fishjam/_openapi_client/ \ + --output-path={output_path} \ --custom-template-path=templates/openapi" ) +def update_client(): + _generate_client( + _spec_argument("fishjam openapi.yaml"), + "openapi-python-client-config.yaml", + "fishjam/_openapi_client/", + ) + + +def update_composition_client(): + _generate_client( + _spec_argument("Composition API openapi.json"), + "openapi-python-client-composition-config.yaml", + "fishjam/_composition_client/", + ) + + def start_room_manager(): current_path = os.getcwd() current_folder = os.path.basename(current_path) From a01e093f373ebcd721ae35be8128c94ea252cce3 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 26 Aug 2026 16:45:48 +0200 Subject: [PATCH 2/7] Add CompositionClient Wraps the generated bindings the way FishjamClient wraps its own, with a register method per input and output variant because the response differs per type. Models are exported from fishjam.composition. --- fishjam/__init__.py | 21 +- fishjam/api/_composition_client.py | 721 +++++++++++++++++++++++++++++ fishjam/composition/__init__.py | 183 ++++++++ fishjam/errors.py | 67 ++- fishjam/utils.py | 17 + tests/fixtures/font.ttf | 1 + tests/test_composition.py | 419 +++++++++++++++++ 7 files changed, 1410 insertions(+), 19 deletions(-) create mode 100644 fishjam/api/_composition_client.py create mode 100644 fishjam/composition/__init__.py create mode 100644 tests/fixtures/font.ttf create mode 100644 tests/test_composition.py diff --git a/fishjam/__init__.py b/fishjam/__init__.py index d3e2017..99002a5 100644 --- a/fishjam/__init__.py +++ b/fishjam/__init__.py @@ -8,7 +8,17 @@ # pylint: disable=locally-disabled, no-name-in-module, import-error # Exceptions and Server Messages -from fishjam import agent, errors, events, integrations, peer, recording, room, version +from fishjam import ( + agent, + composition, + errors, + events, + integrations, + peer, + recording, + room, + version, +) from fishjam._openapi_client.models import PeerMetadata # API @@ -18,6 +28,11 @@ verify_webhook_signature, ) from fishjam._ws_notifier import FishjamNotifier +from fishjam.api._composition_client import ( + CompositionClient, + Mp4InputDurations, + WhipInputTarget, +) from fishjam.api._fishjam_client import ( AgentOptions, AgentOutputOptions, @@ -40,6 +55,9 @@ __all__ = [ "FishjamClient", + "CompositionClient", + "WhipInputTarget", + "Mp4InputDurations", "FishjamNotifier", "decode_server_notifications", "receive_binary", @@ -64,6 +82,7 @@ "recording", "agent", "integrations", + "composition", ] diff --git a/fishjam/api/_composition_client.py b/fishjam/api/_composition_client.py new file mode 100644 index 0000000..f0ddb17 --- /dev/null +++ b/fishjam/api/_composition_client.py @@ -0,0 +1,721 @@ +"""Composition client used to manage compositions, the video compositing sessions.""" + +from dataclasses import dataclass +from http import HTTPStatus +from io import BytesIO +from pathlib import Path +from typing import Any, TypeVar, cast +from urllib.parse import quote + +from fishjam._composition_client.api.compositions import ( + create_composition as compositions_create, +) +from fishjam._composition_client.api.compositions import ( + delete_composition as compositions_delete, +) +from fishjam._composition_client.api.compositions import ( + reset as compositions_reset, +) +from fishjam._composition_client.api.compositions import ( + start as compositions_start, +) +from fishjam._composition_client.api.events import ( + send_composition_event as events_send, +) +from fishjam._composition_client.api.inputs import ( + register_input as inputs_register, +) +from fishjam._composition_client.api.inputs import ( + unregister_input as inputs_unregister, +) +from fishjam._composition_client.api.outputs import ( + register_output as outputs_register, +) +from fishjam._composition_client.api.outputs import ( + register_template_output as outputs_register_template, +) +from fishjam._composition_client.api.outputs import ( + request_keyframe as outputs_request_keyframe, +) +from fishjam._composition_client.api.outputs import ( + unregister_output as outputs_unregister, +) +from fishjam._composition_client.api.outputs import ( + update_output as outputs_update, +) +from fishjam._composition_client.api.renderers import ( + register_font as renderers_register_font, +) +from fishjam._composition_client.api.renderers import ( + register_image as renderers_register_image, +) +from fishjam._composition_client.api.renderers import ( + unregister_image as renderers_unregister_image, +) +from fishjam._composition_client.client import AuthenticatedClient +from fishjam._composition_client.errors import UnexpectedStatus +from fishjam._composition_client.models import ( + ApiError, + AudioScene, + CompositionCreatedResponse, + CreateCompositionRequest, + Mp4Input, + Mp4InputType, + OutputRtmpClientAudioOptions, + OutputRtmpClientVideoOptions, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + RegisterFontBody, + RegisterInputResponse, + RegisterTemplateOutputBody, + RtmpInput, + RtmpInputType, + RtmpOutput, + RtmpOutputType, + SendCompositionEventBody, + UnregisterInput, + UnregisterOutput, + UnregisterRenderer, + UpdateOutputRequest, + VideoScene, + WhepInput, + WhepInputType, + WhipInput, + WhipInputType, + WhipOutput, + WhipOutputType, +) +from fishjam._composition_client.types import UNSET, File, Unset +from fishjam.composition import ( + FileSource, + ImageSpec, + RegisterInput, + RegisterOutput, +) +from fishjam.errors import ( + CompositionNotFoundError, + HTTPError, + InputNotFoundError, + InternalServerError, + OutputNotFoundError, + RendererNotFoundError, + error_for_status, +) +from fishjam.utils import get_composition_url +from fishjam.version import get_version + +T = TypeVar("T") + + +@dataclass +class WhipInputTarget: + """Where to publish a WHIP input. + + The input is registered with `CompositionClient.register_whip_input`. Hand + these to a WHIP publisher, such as `useLivestreamStreamer` in the React + client SDK. + + Attributes: + url: Address to publish to. + bearer_token: Token authorizing the publisher. + """ + + url: str + """Address to publish to""" + bearer_token: str + """Token authorizing the publisher""" + + +@dataclass +class Mp4InputDurations: + """How much media an MP4 input holds. + + The input is registered with `CompositionClient.register_mp4_input`. + + Attributes: + video_duration_ms: Length of the video track, when the file has one. + audio_duration_ms: Length of the audio track, when the file has one. + """ + + video_duration_ms: int | None + """Length of the video track, when the file has one""" + audio_duration_ms: int | None + """Length of the audio track, when the file has one""" + + +def _to_error( + status_code: HTTPStatus, error: ApiError | None, not_found: type[HTTPError] +) -> HTTPError: + """Turn a failed Composition API response into the matching Fishjam error. + + Args: + status_code: Status the Composition API responded with. + error: Parsed error body, when the response carried one. + not_found: Error class describing the resource a 404 refers to. + + Returns: + The error to raise. + """ + return error_for_status(status_code, [error.message] if error else [], not_found) + + +def _to_file(source: FileSource, name: str) -> File: + """Read an upload from bytes or from a path. + + The upload is named, so it is sent as a file rather than a plain form field. + Never pass a path taken from untrusted input, since its contents are uploaded. + + Args: + source: The bytes to upload, or a path to read them from. + name: Name to send the upload under, when the source has none of its own. + + Returns: + The upload, as the generated client takes it. + """ + if isinstance(source, bytes): + return File(payload=BytesIO(source), file_name=name) + + path = Path(source) + + return File(payload=BytesIO(path.read_bytes()), file_name=path.name) + + +class CompositionClient: + """Client class that allows to manage compositions. + + A composition is a real-time video compositing session of a Fishjam App. It + requires the management token that can be retrieved from the Fishjam Dashboard, + the same one used by `fishjam.FishjamClient`. + + Example usage: + ```python + client = CompositionClient(management_token="your-management-token") + ``` + """ + + def __init__(self, management_token: str, composition_url: str | None = None): + """Create a client talking to the Composition API. + + Args: + management_token: Secret token authorizing to perform actions on your + account. It is the same token `fishjam.FishjamClient` is configured + with. Never share this token with anyone. + composition_url: Address of the Composition API. Only needs setting when + running against a deployment other than production. + """ + self._url = get_composition_url(composition_url) + self.client = AuthenticatedClient( + self._url, + token=management_token, + headers={"x-fishjam-api-client": f"python-server/{get_version()}"}, + raise_on_unexpected_status=True, + ) + + def _request( + self, + method, + not_found: type[HTTPError] = CompositionNotFoundError, + **kwargs, + ): + try: + response = method.sync_detailed(client=self.client, **kwargs) + except UnexpectedStatus as status: + raise error_for_status( + HTTPStatus(status.status_code), + [status.content.decode(errors="replace")], + not_found, + ) from status + + if isinstance(response.parsed, ApiError): + raise _to_error(response.status_code, response.parsed, not_found) + + return response.parsed + + def composition_url(self, composition_id: str) -> str: + """The address of a composition, as other services refer to it. + + Fishjam needs it to forward a room's tracks with + `fishjam.FishjamClient.forward_room_tracks`. + + Args: + composition_id: ID of the composition. + + Returns: + The address of the composition. + """ + return f"{self._url}/api/composition/{composition_id}" + + def create_composition( + self, config: CreateCompositionRequest | None = None + ) -> CompositionCreatedResponse: + """Create a new composition. + + Inputs registered on it are composed into the scenes its outputs render. + + Args: + config: Configuration of the composition. + + Returns: + The created composition. + """ + return cast( + CompositionCreatedResponse, + self._request( + compositions_create, body=config or CreateCompositionRequest() + ), + ) + + def start_composition(self, composition_id: str) -> None: + """Start a composition created with `autostart` disabled. + + Its outputs begin producing audio and video. + + Args: + composition_id: ID of the composition. + """ + self._request(compositions_start, composition_id=composition_id) + + def reset_composition(self, composition_id: str) -> None: + """Reset a composition, tearing down its scene but keeping it alive. + + Args: + composition_id: ID of the composition. + """ + self._request(compositions_reset, composition_id=composition_id) + + def delete_composition(self, composition_id: str) -> None: + """Delete an existing composition. Its inputs and outputs are torn down with it. + + Args: + composition_id: ID of the composition. + """ + self._request(compositions_delete, composition_id=composition_id) + + def register_input( + self, + composition_id: str, + input_id: str, + input_: RegisterInput, + ) -> RegisterInputResponse: + """Register a media source on a composition. + + Prefer the variant methods, such as + `CompositionClient.register_whip_input`, which return what that input + type produces. + + Args: + composition_id: ID of the composition. + input_id: ID to register the input under. + input_: Configuration of the input. + + Returns: + Whatever the input type produces on registration. + """ + return cast( + RegisterInputResponse, + self._request( + inputs_register, + composition_id=composition_id, + input_id=input_id, + body=input_, + ), + ) + + def register_whip_input( + self, + composition_id: str, + input_id: str, + *, + bearer_token: str | None = None, + video: bool | None = None, + ) -> WhipInputTarget: + """Register an input that a WHIP publisher pushes media into. + + Args: + composition_id: ID of the composition. + input_id: ID to register the input under. + bearer_token: Token the publisher authenticates with. The server picks one + when it is not given. + video: Whether the input accepts an h264-encoded video track. + + Returns: + The address and token to publish with. + + Raises: + InternalServerError: When neither the caller nor the server provides a + token, leaving the input impossible to publish to. + """ + response = self.register_input( + composition_id, + input_id, + WhipInput( + type_=WhipInputType.WHIP_SERVER, + bearer_token=_or_unset(bearer_token), + video=_or_unset(video), + ), + ) + + token = _or_none(response.bearer_token) or bearer_token + if not token: + raise InternalServerError([ + f'Registering WHIP input "{input_id}" returned no bearer token, ' + "so it cannot be published to" + ]) + + route = _or_none(response.endpoint_route) or f"/whip/{quote(input_id, safe='')}" + + return WhipInputTarget( + url=f"{self.composition_url(composition_id)}{route}", bearer_token=token + ) + + def register_whep_input( + self, + composition_id: str, + input_id: str, + *, + endpoint_url: str, + bearer_token: str | None = None, + video: bool | None = None, + ) -> None: + """Register an input that pulls media from a WHEP endpoint. + + Args: + composition_id: ID of the composition. + input_id: ID to register the input under. + endpoint_url: Address of the WHEP endpoint to pull from. + bearer_token: Token to authenticate with. + video: Whether the input accepts an h264-encoded video track. + """ + self.register_input( + composition_id, + input_id, + WhepInput( + type_=WhepInputType.WHEP_CLIENT, + endpoint_url=endpoint_url, + bearer_token=_or_unset(bearer_token), + video=_or_unset(video), + ), + ) + + def register_mp4_input( + self, + composition_id: str, + input_id: str, + *, + url: str, + loop: bool | None = None, + ) -> Mp4InputDurations: + """Register an input that plays an MP4 file. + + Args: + composition_id: ID of the composition. + input_id: ID to register the input under. + url: Address of the file to play. + loop: Whether the file restarts when it ends. + + Returns: + How much media the file holds. + """ + response = self.register_input( + composition_id, + input_id, + Mp4Input(type_=Mp4InputType.MP4, url=url, loop=_or_unset(loop)), + ) + + return Mp4InputDurations( + video_duration_ms=_or_none(response.video_duration_ms), + audio_duration_ms=_or_none(response.audio_duration_ms), + ) + + def register_rtmp_input( + self, composition_id: str, input_id: str, *, stream_key: str + ) -> None: + """Register an input that an RTMP publisher pushes media into. + + The stream key identifies the input; the address to publish to belongs to the + composition, not to this call. + + Args: + composition_id: ID of the composition. + input_id: ID to register the input under. + stream_key: Key the publisher identifies the input with. + """ + self.register_input( + composition_id, + input_id, + RtmpInput(type_=RtmpInputType.RTMP_SERVER, stream_key=stream_key), + ) + + def unregister_input( + self, + composition_id: str, + input_id: str, + options: UnregisterInput | None = None, + ) -> None: + """Unregister an input. Scenes referencing it stop receiving its media. + + Args: + composition_id: ID of the composition. + input_id: ID of the input. + options: When to unregister the input. + """ + self._request( + inputs_unregister, + not_found=InputNotFoundError, + composition_id=composition_id, + input_id=input_id, + body=options or UnregisterInput(), + ) + + def register_output( + self, composition_id: str, output_id: str, output: RegisterOutput + ) -> None: + """Register an output, the destination the composed result is sent to. + + Args: + composition_id: ID of the composition. + output_id: ID to register the output under. + output: Configuration of the output, carrying the scene to render. + """ + self._request( + outputs_register, + composition_id=composition_id, + output_id=output_id, + body=output, + ) + + def register_template_output( + self, + composition_id: str, + output_id: str, + config: RegisterOutput, + template: FileSource, + ) -> None: + """Register an output rendering a template bundle. + + The bundle is built by `@fishjam-cloud/composition-cli`. Never pass a path taken + from untrusted input, since its contents are uploaded. + + A template rebuilds both scenes from React, so `video.initial` and + `audio.initial` are ignored. Omitting `audio` entirely still means no audio + track at all, so pass an audio option with an empty scene when the output + should carry audio. + + Args: + composition_id: ID of the composition. + output_id: ID to register the output under. + config: Configuration of the output. + template: The bundle to render, as bytes or a path to read them from. + """ + self._request( + outputs_register_template, + composition_id=composition_id, + output_id=output_id, + body=RegisterTemplateOutputBody( + config=config, template=_to_file(template, "template.js") + ), + ) + + def register_whip_output( + self, + composition_id: str, + output_id: str, + *, + endpoint_url: str, + bearer_token: str | None = None, + video: OutputWhipVideoOptions | None = None, + audio: OutputWhipAudioOptions | None = None, + ) -> None: + """Register an output sending the composed result to a WHIP endpoint. + + Args: + composition_id: ID of the composition. + output_id: ID to register the output under. + endpoint_url: Address of the WHIP endpoint to publish to. + bearer_token: Token to authenticate with. + video: Video options, carrying the scene to render. + audio: Audio options, carrying the scene to mix. + """ + self.register_output( + composition_id, + output_id, + WhipOutput( + type_=WhipOutputType.WHIP_CLIENT, + endpoint_url=endpoint_url, + bearer_token=_or_unset(bearer_token), + video=_or_unset(video), + audio=_or_unset(audio), + ), + ) + + def register_rtmp_output( + self, + composition_id: str, + output_id: str, + *, + url: str, + video: OutputRtmpClientVideoOptions | None = None, + audio: OutputRtmpClientAudioOptions | None = None, + ) -> None: + """Register an output sending the composed result to an RTMP endpoint. + + Args: + composition_id: ID of the composition. + output_id: ID to register the output under. + url: Address of the RTMP endpoint to publish to. + video: Video options, carrying the scene to render. + audio: Audio options, carrying the scene to mix. + """ + self.register_output( + composition_id, + output_id, + RtmpOutput( + type_=RtmpOutputType.RTMP_CLIENT, + url=url, + video=_or_unset(video), + audio=_or_unset(audio), + ), + ) + + def unregister_output( + self, + composition_id: str, + output_id: str, + options: UnregisterOutput | None = None, + ) -> None: + """Unregister an output. It stops producing audio and video. + + Args: + composition_id: ID of the composition. + output_id: ID of the output. + options: When to unregister the output. + """ + self._request( + outputs_unregister, + not_found=OutputNotFoundError, + composition_id=composition_id, + output_id=output_id, + body=options or UnregisterOutput(), + ) + + def update_output( + self, + composition_id: str, + output_id: str, + update: UpdateOutputRequest | None = None, + *, + video: VideoScene | None = None, + audio: AudioScene | None = None, + ) -> None: + """Replace the scenes an output renders. + + An update has to mirror the registration: whatever the output was + registered with, video, audio or both, has to be given here too, and + whatever it was registered without cannot be. + + Args: + composition_id: ID of the composition. + output_id: ID of the output. + update: The update to apply. + video: The video scene to render, when no full update is given. + audio: The audio scene to mix, when no full update is given. + """ + self._request( + outputs_update, + composition_id=composition_id, + output_id=output_id, + body=update + or UpdateOutputRequest(video=_or_unset(video), audio=_or_unset(audio)), + ) + + def request_keyframe(self, composition_id: str, output_id: str) -> None: + """Ask an output to emit a keyframe. + + A viewer joining mid-stream then renders a full picture sooner. + + Args: + composition_id: ID of the composition. + output_id: ID of the output. + """ + self._request( + outputs_request_keyframe, + composition_id=composition_id, + output_id=output_id, + ) + + def register_image( + self, composition_id: str, image_id: str, image: ImageSpec + ) -> None: + """Register an image that scenes can reference by its renderer ID. + + Args: + composition_id: ID of the composition. + image_id: ID to register the image under. + image: Where to fetch the image from and how to decode it. + """ + self._request( + renderers_register_image, + composition_id=composition_id, + image_id=image_id, + body=image, + ) + + def register_font(self, composition_id: str, font: FileSource) -> None: + """Register a font that scenes can render text with. + + Never pass a path taken from untrusted input, since its contents are uploaded. + + Args: + composition_id: ID of the composition. + font: The font to upload, as bytes or a path to read them from. + """ + self._request( + renderers_register_font, + composition_id=composition_id, + body=RegisterFontBody(font=_to_file(font, "font")), + ) + + def unregister_image( + self, + composition_id: str, + image_id: str, + options: UnregisterRenderer | None = None, + ) -> None: + """Unregister a previously registered image. + + Args: + composition_id: ID of the composition. + image_id: ID of the image. + options: When to unregister the image. + """ + self._request( + renderers_unregister_image, + not_found=RendererNotFoundError, + composition_id=composition_id, + image_id=image_id, + body=options or UnregisterRenderer(), + ) + + def send_event( + self, composition_id: str, event_name: str, data: Any = None + ) -> None: + """Deliver an event to the templates rendered by the composition's outputs. + + Args: + composition_id: ID of the composition. + event_name: Name the template listens for. + data: Payload the template receives with the event. + """ + self._request( + events_send, + composition_id=composition_id, + body=SendCompositionEventBody(event_name=event_name, data=_or_unset(data)), + ) + + +def _or_unset(value: T | None) -> T | Unset: + return UNSET if value is None else value + + +def _or_none(value: T | Unset) -> T | None: + return None if isinstance(value, Unset) else value diff --git a/fishjam/composition/__init__.py b/fishjam/composition/__init__.py new file mode 100644 index 0000000..dd2a320 --- /dev/null +++ b/fishjam/composition/__init__.py @@ -0,0 +1,183 @@ +from pathlib import Path + +from fishjam._composition_client.models import ( + AudioChannels, + AudioMixingStrategy, + AudioScene, + AudioSceneInput, + AverageAndMaxBitrate, + BoxShadow, + CompositionCreatedResponse, + CreateCompositionRequest, + EasingFunctionBounce, + EasingFunctionBounceFunctionName, + EasingFunctionCubicBezier, + EasingFunctionCubicBezierFunctionName, + EasingFunctionLinear, + EasingFunctionLinearFunctionName, + FontUpload, + H264EncoderPreset, + HorizontalAlign, + Image, + ImageSpecAuto, + ImageSpecAutoAssetType, + ImageSpecGif, + ImageSpecGifAssetType, + ImageSpecJpeg, + ImageSpecJpegAssetType, + ImageSpecPng, + ImageSpecPngAssetType, + ImageSpecSvg, + ImageSpecSvgAssetType, + ImageType, + InputStream, + InputStreamType, + Interpolation, + Mp4Input, + Mp4InputType, + OpusEncoderPreset, + OutputEndCondition, + OutputRtmpClientAudioOptions, + OutputRtmpClientVideoOptions, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + Overflow, + PixelFormat, + RegisterInputResponse, + RegisterTemplateOutput, + RescaleMode, + Rescaler, + RescalerType, + Resolution, + RtmpInput, + RtmpInputType, + RtmpOutput, + RtmpOutputType, + Text, + TextStyle, + TextType, + TextWeight, + TextWrapMode, + Tiles, + TilesType, + Transition, + TransportProtocol, + UnregisterInput, + UnregisterOutput, + UnregisterRenderer, + UpdateOutputRequest, + VerticalAlign, + VideoScene, + View, + ViewDirection, + ViewType, + WhepInput, + WhepInputType, + WhipAudioEncoderOptionsAny, + WhipAudioEncoderOptionsAnyType, + WhipAudioEncoderOptionsOpus, + WhipAudioEncoderOptionsOpusType, + WhipInput, + WhipInputType, + WhipOutput, + WhipOutputType, +) + +FileSource = bytes | str | Path +"""Where an upload comes from: the bytes themselves, or a path to read them from.""" + +ImageSpec = ImageSpecAuto | ImageSpecGif | ImageSpecJpeg | ImageSpecPng | ImageSpecSvg +"""How an image is fetched and decoded, one member per asset type.""" + +RegisterInput = Mp4Input | RtmpInput | WhepInput | WhipInput +"""Configuration of a media source, one member per input type.""" + +RegisterOutput = RtmpOutput | WhipOutput +"""Configuration of a destination, one member per output type.""" + +__all__ = [ + "FileSource", + "ImageSpec", + "RegisterInput", + "RegisterOutput", + "AudioChannels", + "AudioMixingStrategy", + "AudioScene", + "AudioSceneInput", + "AverageAndMaxBitrate", + "BoxShadow", + "CompositionCreatedResponse", + "CreateCompositionRequest", + "EasingFunctionBounce", + "EasingFunctionBounceFunctionName", + "EasingFunctionCubicBezier", + "EasingFunctionCubicBezierFunctionName", + "EasingFunctionLinear", + "EasingFunctionLinearFunctionName", + "FontUpload", + "H264EncoderPreset", + "HorizontalAlign", + "Image", + "ImageSpecAuto", + "ImageSpecAutoAssetType", + "ImageSpecGif", + "ImageSpecGifAssetType", + "ImageSpecJpeg", + "ImageSpecJpegAssetType", + "ImageSpecPng", + "ImageSpecPngAssetType", + "ImageSpecSvg", + "ImageSpecSvgAssetType", + "ImageType", + "InputStream", + "InputStreamType", + "Interpolation", + "Mp4Input", + "Mp4InputType", + "OpusEncoderPreset", + "OutputEndCondition", + "OutputRtmpClientAudioOptions", + "OutputRtmpClientVideoOptions", + "OutputWhipAudioOptions", + "OutputWhipVideoOptions", + "Overflow", + "PixelFormat", + "RegisterInputResponse", + "RegisterTemplateOutput", + "RescaleMode", + "Rescaler", + "RescalerType", + "Resolution", + "RtmpInput", + "RtmpInputType", + "RtmpOutput", + "RtmpOutputType", + "Text", + "TextStyle", + "TextType", + "TextWeight", + "TextWrapMode", + "Tiles", + "TilesType", + "Transition", + "TransportProtocol", + "UnregisterInput", + "UnregisterOutput", + "UnregisterRenderer", + "UpdateOutputRequest", + "VerticalAlign", + "VideoScene", + "View", + "ViewDirection", + "ViewType", + "WhepInput", + "WhepInputType", + "WhipAudioEncoderOptionsAny", + "WhipAudioEncoderOptionsAnyType", + "WhipAudioEncoderOptionsOpus", + "WhipAudioEncoderOptionsOpusType", + "WhipInput", + "WhipInputType", + "WhipOutput", + "WhipOutputType", +] diff --git a/fishjam/errors.py b/fishjam/errors.py index 9202ea4..cb588a1 100644 --- a/fishjam/errors.py +++ b/fishjam/errors.py @@ -31,42 +31,46 @@ def from_response(response: Response[Error]): else: errors = response.content.decode(errors="replace") - match response.status_code: - case HTTPStatus.BAD_REQUEST: - return BadRequestError(errors) + return error_for_status(response.status_code, errors) - case HTTPStatus.UNAUTHORIZED: - return UnauthorizedError(errors) - case HTTPStatus.PAYMENT_REQUIRED: - return QuotaExceededError(errors) +class BadRequestError(HTTPError): + def __init__(self, errors): + """@private""" + super().__init__(errors) - case HTTPStatus.NOT_FOUND: - return NotFoundError(errors) - case HTTPStatus.SERVICE_UNAVAILABLE: - return ServiceUnavailableError(errors) +class UnauthorizedError(HTTPError): + def __init__(self, errors): + """@private""" + super().__init__(errors) - case HTTPStatus.CONFLICT: - return ConflictError(errors) - case _: - return InternalServerError(errors) +class NotFoundError(HTTPError): + def __init__(self, errors): + """@private""" + super().__init__(errors) -class BadRequestError(HTTPError): +class CompositionNotFoundError(NotFoundError): def __init__(self, errors): """@private""" super().__init__(errors) -class UnauthorizedError(HTTPError): +class InputNotFoundError(NotFoundError): def __init__(self, errors): """@private""" super().__init__(errors) -class NotFoundError(HTTPError): +class OutputNotFoundError(NotFoundError): + def __init__(self, errors): + """@private""" + super().__init__(errors) + + +class RendererNotFoundError(NotFoundError): def __init__(self, errors): """@private""" super().__init__(errors) @@ -100,3 +104,30 @@ class InvalidFishjamCredentialsError(HTTPError): def __init__(self, errors): """@private""" super().__init__(errors) + + +def error_for_status( + status_code: HTTPStatus, messages, not_found: type["HTTPError"] | None = None +) -> HTTPError: + """@private""" + match status_code: + case HTTPStatus.BAD_REQUEST | HTTPStatus.UNPROCESSABLE_ENTITY: + return BadRequestError(messages) + + case HTTPStatus.UNAUTHORIZED: + return UnauthorizedError(messages) + + case HTTPStatus.PAYMENT_REQUIRED: + return QuotaExceededError(messages) + + case HTTPStatus.NOT_FOUND: + return (not_found or NotFoundError)(messages) + + case HTTPStatus.CONFLICT: + return ConflictError(messages) + + case HTTPStatus.SERVICE_UNAVAILABLE: + return ServiceUnavailableError(messages) + + case _: + return InternalServerError(messages) diff --git a/fishjam/utils.py b/fishjam/utils.py index 2434966..64382ed 100644 --- a/fishjam/utils.py +++ b/fishjam/utils.py @@ -2,6 +2,8 @@ from fishjam.errors import MissingFishjamIdError +COMPOSITION_HOST = "https://rtc.fishjam.io" + def validate_url(url: str) -> bool: try: @@ -19,3 +21,18 @@ def get_fishjam_url(fishjam_id: str) -> str: return f"https://fishjam.io/api/v1/connect/{fishjam_id}" return fishjam_id + + +def get_composition_url(composition_url: str | None = None) -> str: + """Resolve the address of the Composition API, keeping only its origin. + + Args: + composition_url: Address of the Composition API. Only needs setting when + running against a deployment other than production. + + Returns: + The origin the Composition API is reached at. + """ + url = urlparse(composition_url or COMPOSITION_HOST) + + return f"{url.scheme}://{url.netloc}" diff --git a/tests/fixtures/font.ttf b/tests/fixtures/font.ttf new file mode 100644 index 0000000..2a843c7 --- /dev/null +++ b/tests/fixtures/font.ttf @@ -0,0 +1 @@ +font-bytes \ No newline at end of file diff --git a/tests/test_composition.py b/tests/test_composition.py new file mode 100644 index 0000000..0ff5b05 --- /dev/null +++ b/tests/test_composition.py @@ -0,0 +1,419 @@ +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + +import httpx +import pytest + +from fishjam import CompositionClient +from fishjam.composition import ( + AudioScene, + CreateCompositionRequest, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + Resolution, + VideoScene, + View, + ViewType, + WhipOutput, + WhipOutputType, +) +from fishjam.errors import ( + BadRequestError, + CompositionNotFoundError, + InputNotFoundError, + InternalServerError, + OutputNotFoundError, + QuotaExceededError, + RendererNotFoundError, +) +from fishjam.utils import get_composition_url + +COMPOSITION_ID = "comp-1" +INPUT_ID = "cam" +OUTPUT_ID = "out-1" +IMAGE_ID = "logo" +LOCAL_URL = "http://localhost:8000" +FONT_PATH = Path(__file__).parent / "fixtures" / "font.ttf" + + +def client(composition_url: str | None = None) -> CompositionClient: + return CompositionClient(management_token="token", composition_url=composition_url) + + +@contextmanager +def mock_response(body: dict | None = None, status: int = 200): + requests: list[httpx.Request] = [] + + def handle_request(request: httpx.Request, **_kwargs): + request.read() + requests.append(request) + return httpx.Response(status, json=body if body is not None else {}) + + with patch.object( + httpx.HTTPTransport, "handle_request", side_effect=handle_request + ): + yield requests + + +def sent_json(requests: list[httpx.Request]) -> dict: + import json + + return json.loads(requests[0].content) + + +class TestCompositionUrl: + def test_defaults_to_the_production_composition_api(self): + assert get_composition_url() == "https://rtc.fishjam.io" + + def test_uses_the_configured_address(self): + assert get_composition_url(LOCAL_URL) == LOCAL_URL + + def test_keeps_the_origin_only_so_paths_are_not_doubled(self): + assert get_composition_url(f"{LOCAL_URL}/") == LOCAL_URL + + def test_addresses_a_composition_on_the_configured_deployment(self): + assert ( + client(LOCAL_URL).composition_url(COMPOSITION_ID) + == f"{LOCAL_URL}/api/composition/comp-1" + ) + + def test_addresses_a_composition_on_production_by_default(self): + assert ( + client().composition_url(COMPOSITION_ID) + == "https://rtc.fishjam.io/api/composition/comp-1" + ) + + +class TestCompositionLifecycle: + def test_creates_a_composition_with_the_default_config(self): + with mock_response( + {"composition_id": "comp-1", "api_url": LOCAL_URL}, status=201 + ) as requests: + composition = client().create_composition() + + assert requests[0].url.path == "/api/composition" + assert sent_json(requests) == { + "autostart": True, + "cleanup_without_inputs": True, + } + assert composition.composition_id == "comp-1" + + def test_creates_a_composition_that_waits_to_be_started(self): + with mock_response( + {"composition_id": "comp-1", "api_url": LOCAL_URL}, status=201 + ) as requests: + client().create_composition(CreateCompositionRequest(autostart=False)) + + assert sent_json(requests)["autostart"] is False + + def test_starts_a_composition(self): + with mock_response() as requests: + client().start_composition(COMPOSITION_ID) + + assert requests[0].method == "POST" + assert requests[0].url.path == "/api/composition/comp-1/start" + + def test_resets_a_composition(self): + with mock_response() as requests: + client().reset_composition(COMPOSITION_ID) + + assert requests[0].method == "POST" + assert requests[0].url.path == "/api/composition/comp-1/reset" + + def test_deletes_a_composition(self): + with mock_response() as requests: + client().delete_composition(COMPOSITION_ID) + + assert requests[0].method == "DELETE" + assert requests[0].url.path == "/api/composition/comp-1" + + def test_reports_an_unparseable_success_rather_than_returning_nothing(self): + with mock_response({"composition_id": "comp-1", "api_url": LOCAL_URL}): + with pytest.raises(InternalServerError): + client().create_composition() + + def test_requests_a_keyframe_from_an_output(self): + with mock_response() as requests: + client().request_keyframe(COMPOSITION_ID, OUTPUT_ID) + + assert requests[0].method == "POST" + assert ( + requests[0].url.path + == "/api/composition/comp-1/output/out-1/request_keyframe" + ) + + +class TestInputVariants: + def test_resolves_the_whip_address_from_the_route_the_server_returned(self): + with mock_response({ + "bearer_token": "tok", + "endpoint_route": "/whip/server-chosen-route", + }): + target = client(LOCAL_URL).register_whip_input(COMPOSITION_ID, INPUT_ID) + + assert target.url == ( + f"{LOCAL_URL}/api/composition/comp-1/whip/server-chosen-route" + ) + assert target.bearer_token == "tok" + + def test_falls_back_to_the_conventional_whip_route(self): + with mock_response({"bearer_token": "tok"}): + target = client(LOCAL_URL).register_whip_input(COMPOSITION_ID, INPUT_ID) + + assert target.url == f"{LOCAL_URL}/api/composition/comp-1/whip/cam" + + def test_url_encodes_the_input_id_in_the_fallback_route(self): + with mock_response({"bearer_token": "tok"}): + target = client(LOCAL_URL).register_whip_input(COMPOSITION_ID, "front cam") + + assert target.url == f"{LOCAL_URL}/api/composition/comp-1/whip/front%20cam" + + def test_keeps_a_caller_supplied_whip_token(self): + with mock_response({"endpoint_route": "/whip/cam"}): + target = client().register_whip_input( + COMPOSITION_ID, INPUT_ID, bearer_token="mine" + ) + + assert target.bearer_token == "mine" + + def test_raises_when_no_whip_token_is_available(self): + with mock_response({"endpoint_route": "/whip/cam"}): + with pytest.raises(InternalServerError): + client().register_whip_input(COMPOSITION_ID, INPUT_ID) + + def test_sends_the_whip_discriminant(self): + with mock_response({"bearer_token": "tok"}) as requests: + client().register_whip_input(COMPOSITION_ID, INPUT_ID, video=True) + + assert sent_json(requests) == {"type": "whip_server", "video": True} + + def test_sends_the_whep_discriminant(self): + with mock_response() as requests: + client().register_whep_input( + COMPOSITION_ID, INPUT_ID, endpoint_url="https://example.com/whep" + ) + + assert sent_json(requests) == { + "type": "whep_client", + "endpoint_url": "https://example.com/whep", + } + + def test_sends_the_mp4_discriminant(self): + with mock_response() as requests: + client().register_mp4_input( + COMPOSITION_ID, INPUT_ID, url="https://example.com/a.mp4" + ) + + assert sent_json(requests) == { + "type": "mp4", + "url": "https://example.com/a.mp4", + } + + def test_sends_the_rtmp_discriminant(self): + with mock_response() as requests: + client().register_rtmp_input(COMPOSITION_ID, INPUT_ID, stream_key="key") + + assert sent_json(requests) == {"type": "rtmp_server", "stream_key": "key"} + + def test_returns_the_durations_of_an_mp4_input(self): + with mock_response({"video_duration_ms": 1000, "audio_duration_ms": 2000}): + durations = client().register_mp4_input( + COMPOSITION_ID, INPUT_ID, url="https://example.com/a.mp4" + ) + + assert durations.video_duration_ms == 1000 + assert durations.audio_duration_ms == 2000 + + def test_leaves_unknown_mp4_durations_empty(self): + with mock_response(): + durations = client().register_mp4_input( + COMPOSITION_ID, INPUT_ID, url="https://example.com/a.mp4" + ) + + assert durations.video_duration_ms is None + assert durations.audio_duration_ms is None + + +class TestOutputVariants: + def test_sends_the_whip_discriminant(self): + with mock_response() as requests: + client().register_whip_output( + COMPOSITION_ID, OUTPUT_ID, endpoint_url="https://example.com/whip" + ) + + assert sent_json(requests) == { + "type": "whip_client", + "endpoint_url": "https://example.com/whip", + } + + def test_sends_the_rtmp_discriminant(self): + with mock_response() as requests: + client().register_rtmp_output( + COMPOSITION_ID, OUTPUT_ID, url="rtmp://example.com/live" + ) + + assert sent_json(requests) == { + "type": "rtmp_client", + "url": "rtmp://example.com/live", + } + + def test_carries_the_scenes_of_a_whip_output(self): + with mock_response() as requests: + client().register_whip_output( + COMPOSITION_ID, + OUTPUT_ID, + endpoint_url="https://example.com/whip", + video=OutputWhipVideoOptions( + resolution=Resolution(width=1280, height=720), + initial=VideoScene(root=View(type_=ViewType.VIEW)), + ), + audio=OutputWhipAudioOptions(initial=AudioScene(inputs=[])), + ) + + assert sent_json(requests) == { + "type": "whip_client", + "endpoint_url": "https://example.com/whip", + "video": { + "resolution": {"width": 1280, "height": 720}, + "initial": {"root": {"type": "view"}}, + }, + "audio": {"initial": {"inputs": []}}, + } + + def test_sends_a_prebuilt_output_as_it_is(self): + output = WhipOutput( + type_=WhipOutputType.WHIP_CLIENT, + endpoint_url="https://example.com/whip", + video=OutputWhipVideoOptions( + resolution=Resolution(width=1280, height=720), + initial=VideoScene(root=View(type_=ViewType.VIEW)), + ), + ) + + with mock_response() as requests: + client().register_output(COMPOSITION_ID, OUTPUT_ID, output) + + assert sent_json(requests) == { + "type": "whip_client", + "endpoint_url": "https://example.com/whip", + "video": { + "resolution": {"width": 1280, "height": 720}, + "initial": {"root": {"type": "view"}}, + }, + } + + +class TestFileUploads: + def test_reads_a_font_from_a_path(self): + with mock_response() as requests: + client().register_font(COMPOSITION_ID, FONT_PATH) + + assert b"font-bytes" in requests[0].content + assert b'filename="font.ttf"' in requests[0].content + + def test_uploads_a_font_as_a_file_not_a_form_field(self): + with mock_response() as requests: + client().register_font(COMPOSITION_ID, b"inline") + + assert b'name="font"; filename=' in requests[0].content + + def test_accepts_font_bytes_as_they_are(self): + with mock_response() as requests: + client().register_font(COMPOSITION_ID, b"inline") + + assert b"inline" in requests[0].content + + def test_uploads_a_template_bundle_alongside_its_output_config(self): + output = WhipOutput( + type_=WhipOutputType.WHIP_CLIENT, + endpoint_url="https://example.com/whip", + ) + + with mock_response() as requests: + client().register_template_output( + COMPOSITION_ID, OUTPUT_ID, output, b"bundle" + ) + + content = requests[0].content + assert b"bundle" in content + assert b'name="template"; filename=' in content + assert b'"whip_client"' in content + + +class TestMissingResources: + def test_reports_a_missing_input_rather_than_a_missing_composition(self): + with mock_response({"message": "gone"}, status=404): + with pytest.raises(InputNotFoundError): + client().unregister_input(COMPOSITION_ID, INPUT_ID) + + def test_reports_a_missing_output_rather_than_a_missing_composition(self): + with mock_response({"message": "gone"}, status=404): + with pytest.raises(OutputNotFoundError): + client().unregister_output(COMPOSITION_ID, OUTPUT_ID) + + def test_reports_a_missing_image_rather_than_a_missing_composition(self): + with mock_response({"message": "gone"}, status=404): + with pytest.raises(RendererNotFoundError): + client().unregister_image(COMPOSITION_ID, IMAGE_ID) + + def test_reports_a_missing_composition_everywhere_else(self): + with mock_response({"message": "gone"}, status=404): + with pytest.raises(CompositionNotFoundError): + client().start_composition(COMPOSITION_ID) + + def test_maps_a_payment_required_to_a_quota_error(self): + with mock_response({"message": "over quota"}, status=402): + with pytest.raises(QuotaExceededError): + client().create_composition() + + def test_maps_an_unprocessable_request_to_a_bad_request(self): + with mock_response({"message": "nope"}, status=422): + with pytest.raises(BadRequestError): + client().send_event(COMPOSITION_ID, "scene", {"layout": "grid"}) + + +class TestUpdateOutput: + def test_sends_both_scenes_so_the_update_mirrors_the_registration(self): + with mock_response() as requests: + client().update_output( + COMPOSITION_ID, + OUTPUT_ID, + video=VideoScene(root=View(type_=ViewType.VIEW)), + audio=AudioScene(inputs=[]), + ) + + assert sent_json(requests) == { + "video": {"root": {"type": "view"}}, + "audio": {"inputs": []}, + } + + +class TestPublicModels: + ENVELOPES = { + "ApiError", + "EmptyResponse", + "RegisterFontBody", + "RegisterTemplateOutputBody", + "SendCompositionEventBody", + } + + ALIASES = {"FileSource", "ImageSpec", "RegisterInput", "RegisterOutput"} + + def test_exports_every_model_except_the_wire_envelopes(self): + import fishjam.composition as public + from fishjam._composition_client import models as generated + + assert set(public.__all__) == ( + set(generated.__all__) - self.ENVELOPES | self.ALIASES + ) + + def test_exports_the_unions_its_public_methods_declare(self): + import fishjam.composition as public + + assert self.ALIASES <= set(dir(public)) + + def test_keeps_the_request_bodies_the_client_builds_itself_private(self): + import fishjam.composition as public + + assert not self.ENVELOPES & set(dir(public)) From 6b4fc2fc4e5113d8e75b8f704fde1bdd657aed5b Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 26 Aug 2026 16:46:35 +0200 Subject: [PATCH 3/7] Forward a room's tracks into a composition Points Fishjam at a composition and reports the link back on the room. The livestream WHIP and WHEP addresses come with it, since they are derived from the Fishjam URL rather than configured. --- fishjam/api/_client.py | 1 + fishjam/api/_fishjam_client.py | 72 +++++++++++++++-- fishjam/utils.py | 32 ++++++++ tests/test_track_forwarding.py | 143 +++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 tests/test_track_forwarding.py diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index 10541ac..b2fd203 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -13,6 +13,7 @@ class Client: def __init__(self, fishjam_id: str, management_token: str): + self._fishjam_id = fishjam_id self._fishjam_url = get_fishjam_url(fishjam_id) self.client = AuthenticatedClient( self._fishjam_url, diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index b7c352e..c42b502 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -37,6 +37,9 @@ from fishjam._openapi_client.api.streamers import ( generate_streamer_token as streamer_generate_streamer_token, ) +from fishjam._openapi_client.api.track_forwardings import ( + create_track_forwarding as track_forwardings_create, +) from fishjam._openapi_client.api.viewers import ( generate_viewer_token as viewer_generate_viewer_token, ) @@ -44,6 +47,7 @@ AgentOutput, AudioFormat, AudioSampleRate, + CompositionInfo, CompositionSource, ListRecordingsMetadata, MoqAccess, @@ -73,6 +77,7 @@ StreamerToken, SubscribeMode, SubscribeTracksBody, + TrackForwarding, VideoCodec, ViewerToken, WebRTCMetadata, @@ -83,6 +88,7 @@ from fishjam.errors import ( InvalidFishjamCredentialsError, ) +from fishjam.utils import get_livestream_whep_url, get_livestream_whip_url @dataclass @@ -93,6 +99,8 @@ class Room: config: Room configuration. id: Room ID. peers: List of all peers. + composition_info: The composition the room's tracks are forwarded into, + when `FishjamClient.forward_room_tracks` has linked one. """ config: RoomConfig @@ -101,6 +109,8 @@ class Room: """Room ID""" peers: list[Peer] """List of all peers""" + composition_info: CompositionInfo | None = None + """The composition the room's tracks are forwarded into""" @dataclass @@ -359,7 +369,7 @@ def create_room(self, options: RoomOptions | None = None) -> Room: RoomCreateDetailsResponse, self._request(room_create_room, body=config) ).data.room - return Room(config=room.config, id=room.id, peers=room.peers) + return _to_room(room) def get_all_rooms(self) -> list[Room]: """Returns list of all rooms. @@ -369,9 +379,7 @@ def get_all_rooms(self) -> list[Room]: """ rooms = cast(RoomsListingResponse, self._request(room_get_all_rooms)).data - return [ - Room(config=room.config, id=room.id, peers=room.peers) for room in rooms - ] + return [_to_room(room) for room in rooms] def get_room(self, room_id: str) -> Room: """Returns room with the given id. @@ -386,7 +394,7 @@ def get_room(self, room_id: str) -> Room: RoomDetailsResponse, self._request(room_get_room, room_id=room_id) ).data - return Room(config=room.config, id=room.id, peers=room.peers) + return _to_room(room) def delete_peer(self, room_id: str, peer_id: str) -> None: """Deletes a peer from a room. @@ -422,6 +430,47 @@ def refresh_peer_token(self, room_id: str, peer_id: str) -> str: return response.data.token + def forward_room_tracks(self, room_id: str, composition_url: str) -> None: + """Forwards every track published in the room into a composition. + + The composition composes them into its outputs. Pass the composition's + address, as returned by + `fishjam.CompositionClient.composition_url`. + + Args: + room_id: The ID of the room to forward tracks from. + composition_url: The address of the composition to forward tracks to. + """ + self._request( + track_forwardings_create, + room_id=room_id, + body=TrackForwarding(composition_url=composition_url), + ) + + def livestream_whip_url(self) -> str: + """Where to publish a livestream. + + Pair it with a token from + `fishjam.FishjamClient.create_livestream_streamer_token`. A composition + reaches viewers by sending a WHIP output here. + + Returns: + str: The address a WHIP publisher sends the livestream to. + """ + return get_livestream_whip_url(self._fishjam_id) + + def livestream_whep_url(self) -> str: + """Where to watch a livestream. + + Pair it with a token from + `fishjam.FishjamClient.create_livestream_viewer_token`, sent as a bearer + token by the WHEP player. + + Returns: + str: The address a WHEP viewer plays the livestream from. + """ + return get_livestream_whep_url(self._fishjam_id) + def create_livestream_viewer_token(self, room_id: str) -> str: """Generates a viewer token for livestream rooms. @@ -644,3 +693,16 @@ def __parse_peer_metadata(self, metadata: dict | None) -> WebRTCMetadata: peer_metadata.additional_properties[key] = value return peer_metadata + + +def _to_room(room) -> Room: + composition_info = room.composition_info + + return Room( + config=room.config, + id=room.id, + peers=room.peers, + composition_info=None + if isinstance(composition_info, Unset) + else composition_info, + ) diff --git a/fishjam/utils.py b/fishjam/utils.py index 64382ed..9a203a6 100644 --- a/fishjam/utils.py +++ b/fishjam/utils.py @@ -3,6 +3,8 @@ from fishjam.errors import MissingFishjamIdError COMPOSITION_HOST = "https://rtc.fishjam.io" +LIVESTREAM_WHIP_PATH = "/api/v1/live/api/whip" +LIVESTREAM_WHEP_PATH = "/api/v1/live/api/whep" def validate_url(url: str) -> bool: @@ -36,3 +38,33 @@ def get_composition_url(composition_url: str | None = None) -> str: url = urlparse(composition_url or COMPOSITION_HOST) return f"{url.scheme}://{url.netloc}" + + +def _livestream_url(fishjam_id: str, path: str) -> str: + url = urlparse(get_fishjam_url(fishjam_id)) + + return f"{url.scheme}://{url.netloc}{path}" + + +def get_livestream_whip_url(fishjam_id: str) -> str: + """Resolve where a livestream is published, on the same host as Fishjam itself. + + Args: + fishjam_id: The unique identifier for the Fishjam instance. + + Returns: + The address a WHIP publisher sends the livestream to. + """ + return _livestream_url(fishjam_id, LIVESTREAM_WHIP_PATH) + + +def get_livestream_whep_url(fishjam_id: str) -> str: + """Resolve where a livestream is watched, on the same host as Fishjam itself. + + Args: + fishjam_id: The unique identifier for the Fishjam instance. + + Returns: + The address a WHEP viewer plays the livestream from. + """ + return _livestream_url(fishjam_id, LIVESTREAM_WHEP_PATH) diff --git a/tests/test_track_forwarding.py b/tests/test_track_forwarding.py new file mode 100644 index 0000000..a0534b7 --- /dev/null +++ b/tests/test_track_forwarding.py @@ -0,0 +1,143 @@ +import json +from contextlib import contextmanager +from unittest.mock import patch + +import httpx + +from fishjam import CompositionClient, FishjamClient +from fishjam.utils import get_livestream_whep_url, get_livestream_whip_url + +ROOM_ID = "room-1" +COMPOSITION_ID = "comp-1" +LOCAL_COMPOSITION_URL = "http://localhost:8000" +FISHJAM_ID = "abc123" + + +@contextmanager +def mock_response(status: int = 201): + requests: list[httpx.Request] = [] + + def handle_request(request: httpx.Request, **_kwargs): + request.read() + requests.append(request) + return httpx.Response(status, json={}) + + with patch.object( + httpx.HTTPTransport, "handle_request", side_effect=handle_request + ): + yield requests + + +class TestForwardRoomTracks: + def test_points_fishjam_at_the_composition_it_should_feed(self): + compositions = CompositionClient( + management_token="token", composition_url=LOCAL_COMPOSITION_URL + ) + fishjam = FishjamClient(FISHJAM_ID, "token") + + with mock_response() as requests: + fishjam.forward_room_tracks( + ROOM_ID, compositions.composition_url(COMPOSITION_ID) + ) + + assert ( + requests[0].url.path + == "/api/v1/connect/abc123/room/room-1/track_forwardings" + ) + assert requests[0].method == "POST" + assert json.loads(requests[0].content) == { + "compositionURL": f"{LOCAL_COMPOSITION_URL}/api/composition/comp-1", + "selector": "all", + } + + +class TestLivestreamWhipUrl: + def test_derives_the_address_from_a_bare_fishjam_id(self): + assert ( + FishjamClient(FISHJAM_ID, "token").livestream_whip_url() + == "https://fishjam.io/api/v1/live/api/whip" + ) + + def test_keeps_the_host_when_the_fishjam_id_is_a_full_url(self): + client = FishjamClient( + "https://cloud.fishjam.work/api/v1/connect/abc123", "token" + ) + + assert ( + client.livestream_whip_url() + == "https://cloud.fishjam.work/api/v1/live/api/whip" + ) + + def test_derives_the_address_without_a_client(self): + assert ( + get_livestream_whip_url(FISHJAM_ID) + == "https://fishjam.io/api/v1/live/api/whip" + ) + + +class TestLivestreamWhepUrl: + def test_derives_the_address_from_a_bare_fishjam_id(self): + assert ( + FishjamClient(FISHJAM_ID, "token").livestream_whep_url() + == "https://fishjam.io/api/v1/live/api/whep" + ) + + def test_keeps_the_host_when_the_fishjam_id_is_a_full_url(self): + client = FishjamClient( + "https://cloud.fishjam.work/api/v1/connect/abc123", "token" + ) + + assert ( + client.livestream_whep_url() + == "https://cloud.fishjam.work/api/v1/live/api/whep" + ) + + def test_derives_the_address_without_a_client(self): + assert ( + get_livestream_whep_url(FISHJAM_ID) + == "https://fishjam.io/api/v1/live/api/whep" + ) + + +class TestRoomCompositionInfo: + ROOM = { + "id": ROOM_ID, + "config": {"roomType": "conference"}, + "peers": [], + "compositionInfo": { + "compositionUrl": "http://localhost:8000/api/composition/comp-1", + "forwardings": [ + {"inputId": "in-1", "peerId": "peer-1", "videoTrackId": "track-1"} + ], + }, + } + + def test_reports_which_composition_the_room_feeds(self): + def handle_request(request: httpx.Request, **_kwargs): + request.read() + return httpx.Response(200, json={"data": self.ROOM}) + + with patch.object( + httpx.HTTPTransport, "handle_request", side_effect=handle_request + ): + room = FishjamClient(FISHJAM_ID, "token").get_room(ROOM_ID) + + assert room.composition_info is not None + assert ( + room.composition_info.composition_url + == "http://localhost:8000/api/composition/comp-1" + ) + assert room.composition_info.forwardings[0].peer_id == "peer-1" + + def test_leaves_it_empty_when_the_room_feeds_nothing(self): + def handle_request(request: httpx.Request, **_kwargs): + request.read() + room = {k: v for k, v in self.ROOM.items() if k != "compositionInfo"} + return httpx.Response(200, json={"data": room}) + + with patch.object( + httpx.HTTPTransport, "handle_request", side_effect=handle_request + ): + room = FishjamClient(FISHJAM_ID, "token").get_room(ROOM_ID) + + assert room.composition_info is None From 4bae4ee0f75f0a076ba99fa0cd8223449987fe71 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 26 Aug 2026 16:52:50 +0200 Subject: [PATCH 4/7] Add the composition example A backend that composes a looping movie and a camera published over WHIP into a Fishjam livestream, serving the credentials for both ends over HTTP. --- examples/composition/.env.example | 6 + examples/composition/README.md | 82 ++++++++++++ examples/composition/composition/__init__.py | 0 examples/composition/composition/app.py | 60 +++++++++ .../composition/composition_service.py | 83 ++++++++++++ examples/composition/composition/config.py | 24 ++++ .../composition/fishjam_service.py | 24 ++++ examples/composition/composition/scene.py | 124 ++++++++++++++++++ examples/composition/main.py | 32 +++++ examples/composition/pyproject.toml | 15 +++ pyproject.toml | 1 + uv.lock | 20 +++ 12 files changed, 471 insertions(+) create mode 100644 examples/composition/.env.example create mode 100644 examples/composition/README.md create mode 100644 examples/composition/composition/__init__.py create mode 100644 examples/composition/composition/app.py create mode 100644 examples/composition/composition/composition_service.py create mode 100644 examples/composition/composition/config.py create mode 100644 examples/composition/composition/fishjam_service.py create mode 100644 examples/composition/composition/scene.py create mode 100644 examples/composition/main.py create mode 100644 examples/composition/pyproject.toml diff --git a/examples/composition/.env.example b/examples/composition/.env.example new file mode 100644 index 0000000..670ac42 --- /dev/null +++ b/examples/composition/.env.example @@ -0,0 +1,6 @@ +# Your Fishjam envs, which you can get at https://fishjam.io/app +FISHJAM_ID="your-fishjam-id" +FISHJAM_MANAGEMENT_TOKEN="your-management-token" + +# Only needed when running against a deployment other than production +# COMPOSITION_URL="http://localhost:8000" diff --git a/examples/composition/README.md b/examples/composition/README.md new file mode 100644 index 0000000..313bcec --- /dev/null +++ b/examples/composition/README.md @@ -0,0 +1,82 @@ +# Composition Demo + +Demo application showing compositions, the real-time video compositing sessions, with +[Fishjam](https://fishjam.io) and the Python Server SDK. + +Two sources are composed into one picture, laid out like a gaming livestream: a looping +movie fills the stage with your camera tucked into its top corner, framed by the Fishjam +logo and a caption bar. The result is sent to a Fishjam livestream that viewers watch. + +``` +[camera] ── WHIP ─┐ + ├─▶ composition ── WHIP ─▶ fishjam livestream ── WHEP ─▶ [viewers] +[movie mp4] ──────┘ +``` + +## What it shows + +- **Inputs**: a WHIP input the demo prints publishing credentials for, and an MP4 input + looping a movie from a URL +- **Renderers**: an SVG logo registered as an image, and the Inter font used by the caption +- **Scene**: the movie fills the stage with the camera in its top corner, on a cream frame + with a coral bar along the bottom. Each tile sits on a black backing, so a stream that is + not publishing yet reads as an empty tile rather than a hole + - _picture in picture_ — the movie fills the stage, the camera sits in its top corner + - _spotlight_ — the camera takes the stage with the movie tucked away + - _side by side_ — the movie and the camera share the stage +- **Audio**: both inputs mixed, with the movie ducked under the camera + +## Prerequisites + +- Python 3.10+ +- [uv](https://docs.astral.sh/uv/) package manager +- Fishjam credentials ([get them here](https://fishjam.io/app)) + +> [!IMPORTANT] +> All commands should be run from the `examples/composition` directory + +## Quick Start + +1. Install dependencies: + + ```bash + uv sync + ``` + +2. Copy [`.env.example`](./.env.example) to `.env` and populate your environment variables. + +3. Run the server: + + ```bash + uv run ./main.py + ``` + +Starting the server creates the composition and the livestream, then serves two endpoints: + +| Endpoint | Returns | +| --- | --- | +| `GET /streamer` | `url` and `token` to publish a camera into the composition over WHIP | +| `GET /viewer` | `url` and `token` to watch the composed result over WHEP | + +Publish with any WHIP client, such as `useLivestreamStreamer` from the React client SDK, +and watch with a livestream viewer such as `useLivestreamViewer`. + +Press Ctrl+C to delete the composition and the livestream room. + +To change the scene while the output is running, call `CompositionClient.update_output`. +An update has to mirror the registration: this output registers both video and audio, so +an update has to carry both. + +> [!NOTE] +> A composition holds resources until it is deleted, so let the demo clean up on exit +> rather than killing it. + +## Composing a whole room + +This demo composes inputs whose IDs it chooses itself. To compose everyone in a room +instead, forward the room's tracks with `FishjamClient.forward_room_tracks` and render +them with a template, since a template decides the layout as peers come and go. Build one +with `npx @fishjam-cloud/composition-cli build App.tsx --out template.js`, register it +with `CompositionClient.register_template_output`, and see the +[JS example](https://github.com/fishjam-cloud/js-server-sdk/tree/main/examples/composition) +for a template to start from. diff --git a/examples/composition/composition/__init__.py b/examples/composition/composition/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/composition/composition/app.py b/examples/composition/composition/app.py new file mode 100644 index 0000000..4e4c676 --- /dev/null +++ b/examples/composition/composition/app.py @@ -0,0 +1,60 @@ +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response +from starlette.routing import Route + +from .composition_service import CompositionService +from .config import COMPOSITION_URL, FISHJAM_ID, FISHJAM_TOKEN +from .fishjam_service import FishjamService + +fishjam = FishjamService(FISHJAM_ID, FISHJAM_TOKEN) +composition = CompositionService(FISHJAM_TOKEN, COMPOSITION_URL) + + +def start_composing() -> None: + composition.register_assets() + composition.play_movie() + composition.camera() + composition.stream_to( + fishjam.livestream_whip_url(), fishjam.create_streamer_token() + ) + + +def cleanup() -> None: + for service in (composition, fishjam): + try: + service.cleanup() + except Exception as error: + print(f"cleanup failed: {error}") + + +async def streamer(_request: Request) -> Response: + camera = composition.camera() + + return JSONResponse({"url": camera.url, "token": camera.bearer_token}) + + +async def viewer(_request: Request) -> Response: + return JSONResponse({ + "url": fishjam.livestream_whep_url(), + "token": fishjam.create_viewer_token(), + }) + + +app = Starlette( + routes=[ + Route("/streamer", streamer, methods=["GET"]), + Route("/viewer", viewer, methods=["GET"]), + ], + middleware=[ + Middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + ], +) diff --git a/examples/composition/composition/composition_service.py b/examples/composition/composition/composition_service.py new file mode 100644 index 0000000..f0c9ee0 --- /dev/null +++ b/examples/composition/composition/composition_service.py @@ -0,0 +1,83 @@ +import httpx + +from fishjam import CompositionClient, WhipInputTarget +from fishjam.composition import ( + AudioScene, + AudioSceneInput, + ImageSpecSvg, + ImageSpecSvgAssetType, + OutputWhipAudioOptions, + OutputWhipVideoOptions, + Resolution, +) + +from .config import ( + CAMERA_INPUT_ID, + FONT_URL, + HEIGHT, + LOGO_IMAGE_ID, + LOGO_URL, + MOVIE_INPUT_ID, + MOVIE_URL, + OUTPUT_ID, + WIDTH, +) +from .scene import scene + + +class CompositionService: + def __init__(self, management_token: str, composition_url: str | None = None): + self.compositions = CompositionClient( + management_token=management_token, composition_url=composition_url + ) + self.composition_id = self.compositions.create_composition().composition_id + self._camera: WhipInputTarget | None = None + + def register_assets(self) -> None: + self.compositions.register_font( + self.composition_id, httpx.get(FONT_URL, follow_redirects=True).content + ) + self.compositions.register_image( + self.composition_id, + LOGO_IMAGE_ID, + ImageSpecSvg( + asset_type=ImageSpecSvgAssetType.SVG, + url=LOGO_URL, + resolution=Resolution(width=200, height=200), + ), + ) + + def play_movie(self) -> None: + self.compositions.register_mp4_input( + self.composition_id, MOVIE_INPUT_ID, url=MOVIE_URL, loop=True + ) + + def camera(self) -> WhipInputTarget: + if self._camera is None: + self._camera = self.compositions.register_whip_input( + self.composition_id, CAMERA_INPUT_ID, video=True + ) + + return self._camera + + def stream_to(self, endpoint_url: str, bearer_token: str) -> None: + self.compositions.register_whip_output( + self.composition_id, + OUTPUT_ID, + endpoint_url=endpoint_url, + bearer_token=bearer_token, + video=OutputWhipVideoOptions( + resolution=Resolution(width=WIDTH, height=HEIGHT), initial=scene() + ), + audio=OutputWhipAudioOptions( + initial=AudioScene( + inputs=[ + AudioSceneInput(input_id=CAMERA_INPUT_ID), + AudioSceneInput(input_id=MOVIE_INPUT_ID, volume=0.2), + ] + ) + ), + ) + + def cleanup(self) -> None: + self.compositions.delete_composition(self.composition_id) diff --git a/examples/composition/composition/config.py b/examples/composition/composition/config.py new file mode 100644 index 0000000..97b3f82 --- /dev/null +++ b/examples/composition/composition/config.py @@ -0,0 +1,24 @@ +import os + +import dotenv + +dotenv.load_dotenv() + +FISHJAM_ID = os.environ["FISHJAM_ID"] +FISHJAM_TOKEN = os.environ["FISHJAM_MANAGEMENT_TOKEN"] +COMPOSITION_URL = os.getenv("COMPOSITION_URL") +HOST = os.getenv("HOST", "localhost") +PORT = int(os.getenv("PORT", "8000")) + +CAMERA_INPUT_ID = "camera" +MOVIE_INPUT_ID = "movie" +LOGO_IMAGE_ID = "fish" +OUTPUT_ID = "livestream" + +MOVIE_URL = "https://github.com/smelter-labs/smelter-snapshot-tests/raw/refs/heads/main/assets/BigBuckBunny720p24fpsStereo30s.mp4" +LOGO_URL = "https://fishjam.swmansion.com/favicon.svg" +FONT_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf" +FONT_FAMILY = "Inter" + +WIDTH = 1280 +HEIGHT = 720 diff --git a/examples/composition/composition/fishjam_service.py b/examples/composition/composition/fishjam_service.py new file mode 100644 index 0000000..11f204a --- /dev/null +++ b/examples/composition/composition/fishjam_service.py @@ -0,0 +1,24 @@ +from fishjam import FishjamClient, RoomOptions + + +class FishjamService: + def __init__(self, fishjam_id: str, management_token: str): + self.fishjam = FishjamClient(fishjam_id, management_token) + self.livestream_id = self.fishjam.create_room( + RoomOptions(room_type="livestream") + ).id + + def livestream_whip_url(self) -> str: + return self.fishjam.livestream_whip_url() + + def livestream_whep_url(self) -> str: + return self.fishjam.livestream_whep_url() + + def create_streamer_token(self) -> str: + return self.fishjam.create_livestream_streamer_token(self.livestream_id) + + def create_viewer_token(self) -> str: + return self.fishjam.create_livestream_viewer_token(self.livestream_id) + + def cleanup(self) -> None: + self.fishjam.delete_room(self.livestream_id) diff --git a/examples/composition/composition/scene.py b/examples/composition/composition/scene.py new file mode 100644 index 0000000..b475e17 --- /dev/null +++ b/examples/composition/composition/scene.py @@ -0,0 +1,124 @@ +from fishjam.composition import ( + BoxShadow, + HorizontalAlign, + Image, + ImageType, + InputStream, + InputStreamType, + RescaleMode, + Rescaler, + RescalerType, + Text, + TextType, + TextWeight, + VideoScene, + View, + ViewType, +) + +from .config import ( + CAMERA_INPUT_ID, + FONT_FAMILY, + LOGO_IMAGE_ID, + MOVIE_INPUT_ID, + WIDTH, +) + +BAR_TEXT = "COMPOSITION DEMO /// publish a camera over WHIP to join" + +CREAM = "#FCF6E7FF" +CORAL = "#ED716DFF" +BLACK = "#000000FF" +SHADOW = [BoxShadow(color="#00000026", offset_x=0, offset_y=10, blur_radius=24)] + +MARGIN = 48 +LOGO_SIZE = 72 +BAR_HEIGHT = 44 +STAGE_HEIGHT = 450 +STAGE_WIDTH = STAGE_HEIGHT * 16 // 9 + + +def _tile(input_id: str, **placement) -> View: + return View( + type_=ViewType.VIEW, + background_color=BLACK, + border_radius=20, + box_shadow=SHADOW, + children=[ + Rescaler( + type_=RescalerType.RESCALER, + child=InputStream( + type_=InputStreamType.INPUT_STREAM, input_id=input_id + ), + mode=RescaleMode.FILL, + ) + ], + **placement, + ) + + +def scene() -> VideoScene: + stage = View( + type_=ViewType.VIEW, + top=MARGIN * 2 + LOGO_SIZE, + left=(WIDTH - STAGE_WIDTH) // 2, + width=STAGE_WIDTH, + height=STAGE_HEIGHT, + children=[ + _tile( + MOVIE_INPUT_ID, + top=0, + left=0, + width=STAGE_WIDTH, + height=STAGE_HEIGHT, + ), + _tile( + CAMERA_INPUT_ID, + top=18, + right=18, + width=220, + height=124, + border_width=6, + border_color=CORAL, + ), + ], + ) + + logo = View( + type_=ViewType.VIEW, + top=MARGIN, + left=WIDTH - MARGIN - LOGO_SIZE, + width=LOGO_SIZE, + height=LOGO_SIZE, + children=[Image(type_=ImageType.IMAGE, image_id=LOGO_IMAGE_ID)], + ) + + bar = View( + type_=ViewType.VIEW, + bottom=0, + left=0, + width=WIDTH, + height=BAR_HEIGHT, + background_color=CORAL, + padding_horizontal=MARGIN, + padding_vertical=12, + children=[ + Text( + type_=TextType.TEXT, + text=BAR_TEXT, + font_size=20, + font_family=FONT_FAMILY, + weight=TextWeight.SEMI_BOLD, + color=CREAM, + align=HorizontalAlign.LEFT, + ) + ], + ) + + return VideoScene( + root=View( + type_=ViewType.VIEW, + background_color=CREAM, + children=[stage, logo, bar], + ) + ) diff --git a/examples/composition/main.py b/examples/composition/main.py new file mode 100644 index 0000000..8043138 --- /dev/null +++ b/examples/composition/main.py @@ -0,0 +1,32 @@ +import sys +from contextlib import asynccontextmanager + +import uvicorn +from composition.app import app, cleanup, start_composing +from composition.config import HOST, PORT + + +@asynccontextmanager +async def lifespan(_app): + try: + start_composing() + except Exception as error: + print(f"failed to start composing: {error}", file=sys.stderr) + cleanup() + raise SystemExit(1) from error + + print(f"streamer credentials on http://{HOST}:{PORT}/streamer") + print(f"viewer token on http://{HOST}:{PORT}/viewer") + + try: + yield + finally: + cleanup() + print("deleted the composition and the livestream room") + + +app.router.lifespan_context = lifespan + + +if __name__ == "__main__": + uvicorn.run(app, host=HOST, port=PORT, log_level="info") diff --git a/examples/composition/pyproject.toml b/examples/composition/pyproject.toml new file mode 100644 index 0000000..f1ba2f9 --- /dev/null +++ b/examples/composition/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "composition-demo" +version = "0.1.0" +description = "Composition demo using Fishjam Python SDK" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "starlette>=0.35.0", + "uvicorn>=0.25.0", + "fishjam-server-sdk", + "python-dotenv", +] + +[tool.uv.sources] +fishjam-server-sdk = { workspace = true } diff --git a/pyproject.toml b/pyproject.toml index 7c173a7..04de671 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ default-groups = ["dev", "test"] [tool.uv.workspace] members = [ + "examples/composition", "examples/transcription", ".", "examples/poet_chat", diff --git a/uv.lock b/uv.lock index 77954a5..94db53d 100644 --- a/uv.lock +++ b/uv.lock @@ -10,6 +10,7 @@ resolution-markers = [ [manifest] members = [ + "composition-demo", "fishjam-server-sdk", "multimodal", "poet-chat", @@ -366,6 +367,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "composition-demo" +version = "0.1.0" +source = { virtual = "examples/composition" } +dependencies = [ + { name = "fishjam-server-sdk" }, + { name = "python-dotenv" }, + { name = "starlette" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fishjam-server-sdk", editable = "." }, + { name = "python-dotenv" }, + { name = "starlette", specifier = ">=0.35.0" }, + { name = "uvicorn", specifier = ">=0.25.0" }, +] + [[package]] name = "cryptography" version = "48.0.1" From 958301d4226b712fc8b44dcbd282fbd633fc432e Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Thu, 27 Aug 2026 12:14:25 +0200 Subject: [PATCH 5/7] Name both generated clients after the API they wrap Neither _openapi_client nor _composition_client said what the other did. --- examples/room_manager/room_service.py | 2 +- fishjam/__init__.py | 2 +- .../__init__.py | 0 .../api/__init__.py | 0 .../api/compositions/__init__.py | 0 .../api/compositions/create_composition.py | 0 .../api/compositions/delete_composition.py | 0 .../api/compositions/reset.py | 0 .../api/compositions/start.py | 0 .../api/events/__init__.py | 0 .../api/events/send_composition_event.py | 0 .../api/inputs/__init__.py | 0 .../api/inputs/register_input.py | 0 .../api/inputs/unregister_input.py | 0 .../api/media_transport/__init__.py | 0 .../api/outputs/__init__.py | 0 .../api/outputs/register_output.py | 0 .../api/outputs/register_template_output.py | 0 .../api/outputs/request_keyframe.py | 0 .../api/outputs/unregister_output.py | 0 .../api/outputs/update_output.py | 0 .../api/renderers/__init__.py | 0 .../api/renderers/register_font.py | 0 .../api/renderers/register_image.py | 0 .../api/renderers/unregister_image.py | 0 .../client.py | 0 .../errors.py | 0 .../models/__init__.py | 0 .../models/api_error.py | 0 .../models/audio_channels.py | 0 .../models/audio_mixing_strategy.py | 0 .../models/audio_scene.py | 0 .../models/audio_scene_input.py | 0 .../models/average_and_max_bitrate.py | 0 .../models/box_shadow.py | 0 .../models/composition_created_response.py | 0 .../models/create_composition_request.py | 0 .../models/easing_function_bounce.py | 0 .../easing_function_bounce_function_name.py | 0 .../models/easing_function_cubic_bezier.py | 0 ...ing_function_cubic_bezier_function_name.py | 0 .../models/easing_function_linear.py | 0 .../easing_function_linear_function_name.py | 0 .../models/empty_response.py | 0 .../models/font_upload.py | 0 .../models/h264_encoder_preset.py | 0 .../models/horizontal_align.py | 0 .../models/image.py | 0 .../models/image_spec_auto.py | 0 .../models/image_spec_auto_asset_type.py | 0 .../models/image_spec_gif.py | 0 .../models/image_spec_gif_asset_type.py | 0 .../models/image_spec_jpeg.py | 0 .../models/image_spec_jpeg_asset_type.py | 0 .../models/image_spec_png.py | 0 .../models/image_spec_png_asset_type.py | 0 .../models/image_spec_svg.py | 0 .../models/image_spec_svg_asset_type.py | 0 .../models/image_type.py | 0 .../models/input_stream.py | 0 .../models/input_stream_type.py | 0 .../models/interpolation.py | 0 .../models/mp_4_input.py | 0 .../models/mp_4_input_type.py | 0 .../models/opus_encoder_preset.py | 0 .../models/output_end_condition.py | 0 .../output_rtmp_client_audio_options.py | 0 .../output_rtmp_client_video_options.py | 0 .../models/output_whip_audio_options.py | 0 .../models/output_whip_video_options.py | 0 .../models/overflow.py | 0 .../models/pixel_format.py | 0 .../models/register_font_body.py | 0 .../models/register_input_response.py | 0 .../models/register_template_output.py | 0 .../models/register_template_output_body.py | 0 .../models/rescale_mode.py | 0 .../models/rescaler.py | 0 .../models/rescaler_type.py | 0 .../models/resolution.py | 0 .../models/rtmp_input.py | 0 .../models/rtmp_input_type.py | 0 .../models/rtmp_output.py | 0 .../models/rtmp_output_type.py | 0 .../models/send_composition_event_body.py | 0 .../models/text.py | 0 .../models/text_style.py | 0 .../models/text_type.py | 0 .../models/text_weight.py | 0 .../models/text_wrap_mode.py | 0 .../models/tiles.py | 0 .../models/tiles_type.py | 0 .../models/transition.py | 0 .../models/transport_protocol.py | 0 .../models/unregister_input.py | 0 .../models/unregister_output.py | 0 .../models/unregister_renderer.py | 0 .../models/update_output_request.py | 0 .../models/vertical_align.py | 0 .../models/video_scene.py | 0 .../models/view.py | 0 .../models/view_direction.py | 0 .../models/view_type.py | 0 .../models/whep_input.py | 0 .../models/whep_input_type.py | 0 .../models/whip_audio_encoder_options_any.py | 0 .../whip_audio_encoder_options_any_type.py | 0 .../models/whip_audio_encoder_options_opus.py | 0 .../whip_audio_encoder_options_opus_type.py | 0 .../models/whip_input.py | 0 .../models/whip_input_type.py | 0 .../models/whip_output.py | 0 .../models/whip_output_type.py | 0 .../types.py | 0 .../__init__.py | 0 .../api/__init__.py | 0 .../api/credentials/__init__.py | 0 .../api/credentials/validate_credentials.py | 0 .../api/mo_q/__init__.py | 0 .../api/mo_q/create_moq_access.py | 0 .../api/recordings/__init__.py | 0 .../api/recordings/create_recording.py | 0 .../api/recordings/delete_recording.py | 0 .../api/recordings/get_recording.py | 0 .../api/recordings/list_recordings.py | 0 .../api/recordings/stop_recording.py | 0 .../api/rooms/__init__.py | 0 .../api/rooms/add_peer.py | 0 .../api/rooms/create_room.py | 0 .../api/rooms/delete_peer.py | 0 .../api/rooms/delete_room.py | 0 .../api/rooms/get_all_rooms.py | 0 .../api/rooms/get_room.py | 0 .../api/rooms/refresh_token.py | 0 .../api/rooms/subscribe_peer.py | 0 .../api/rooms/subscribe_tracks.py | 0 .../api/streamers/__init__.py | 0 .../api/streamers/create_streamer.py | 0 .../api/streamers/delete_streamer.py | 0 .../api/streamers/generate_streamer_token.py | 0 .../api/streams/__init__.py | 0 .../api/streams/create_stream.py | 0 .../api/streams/delete_stream.py | 0 .../api/streams/get_all_streams.py | 0 .../api/streams/get_stream.py | 0 .../api/track_forwardings/__init__.py | 0 .../create_track_forwarding.py | 0 .../api/viewers/__init__.py | 0 .../api/viewers/create_viewer.py | 0 .../api/viewers/delete_viewer.py | 0 .../api/viewers/generate_viewer_token.py | 0 .../client.py | 0 .../errors.py | 0 .../models/__init__.py | 0 .../models/agent_output.py | 0 .../models/audio_format.py | 0 .../models/audio_sample_rate.py | 0 .../models/composition_info.py | 0 .../models/composition_source.py | 0 .../models/error.py | 0 .../models/list_recordings_metadata.py | 0 .../models/moq_access.py | 0 .../models/moq_access_config.py | 0 .../models/peer.py | 0 .../models/peer_config_agent.py | 0 .../models/peer_config_agent_type.py | 0 .../models/peer_config_vapi.py | 0 .../models/peer_config_vapi_type.py | 0 .../models/peer_config_web_rtc.py | 0 .../models/peer_config_web_rtc_type.py | 0 .../models/peer_details_response.py | 0 .../models/peer_details_response_data.py | 0 .../models/peer_metadata.py | 0 .../models/peer_options_agent.py | 0 .../models/peer_options_vapi.py | 0 .../models/peer_options_web_rtc.py | 0 .../models/peer_refresh_token_response.py | 0 .../peer_refresh_token_response_data.py | 0 .../models/peer_status.py | 0 .../models/peer_type.py | 0 .../models/recording.py | 0 .../models/recording_config.py | 0 .../recording_config_metadata_type_0.py | 0 .../models/recording_details_response.py | 0 .../models/recording_file.py | 0 .../models/recording_list_response.py | 0 .../models/recording_metadata_type_0.py | 0 .../models/recording_status.py | 0 .../models/room.py | 0 .../models/room_config.py | 0 .../models/room_create_details_response.py | 0 .../room_create_details_response_data.py | 0 .../models/room_details_response.py | 0 .../models/room_type.py | 0 .../models/rooms_listing_response.py | 0 .../models/stream.py | 0 .../models/stream_config.py | 0 .../models/stream_details_response.py | 0 .../models/streamer.py | 0 .../models/streamer_details_response.py | 0 .../models/streamer_status.py | 0 .../models/streamer_token.py | 0 .../models/streams_listing_response.py | 0 .../models/subscribe_mode.py | 0 .../models/subscribe_tracks_body.py | 0 .../models/subscriptions.py | 0 .../models/track.py | 0 .../models/track_forwarding.py | 0 .../models/track_forwarding_info.py | 0 .../models/track_metadata.py | 0 .../models/track_type.py | 0 .../models/video_codec.py | 0 .../models/viewer.py | 0 .../models/viewer_details_response.py | 0 .../models/viewer_token.py | 0 .../models/web_rtc_metadata.py | 0 .../py.typed | 0 .../types.py | 0 fishjam/api/_client.py | 6 +-- fishjam/api/_composition_client.py | 38 +++++++------- fishjam/api/_fishjam_client.py | 50 +++++++++++-------- fishjam/composition/__init__.py | 2 +- fishjam/errors.py | 4 +- fishjam/peer/__init__.py | 2 +- fishjam/recording/__init__.py | 2 +- fishjam/room/__init__.py | 2 +- openapi-python-client-composition-config.yaml | 2 +- openapi-python-client-config.yaml | 2 +- pyproject.toml | 8 +-- scripts.py | 6 +-- tests/test_composition.py | 2 +- tests/test_room_api.py | 2 +- 232 files changed, 70 insertions(+), 62 deletions(-) rename fishjam/{_composition_client => _composition_openapi_client}/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/compositions/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/compositions/create_composition.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/compositions/delete_composition.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/compositions/reset.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/compositions/start.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/events/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/events/send_composition_event.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/inputs/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/inputs/register_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/inputs/unregister_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/media_transport/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/outputs/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/outputs/register_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/outputs/register_template_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/outputs/request_keyframe.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/outputs/unregister_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/outputs/update_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/renderers/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/renderers/register_font.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/renderers/register_image.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/api/renderers/unregister_image.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/client.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/errors.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/__init__.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/api_error.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/audio_channels.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/audio_mixing_strategy.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/audio_scene.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/audio_scene_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/average_and_max_bitrate.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/box_shadow.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/composition_created_response.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/create_composition_request.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/easing_function_bounce.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/easing_function_bounce_function_name.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/easing_function_cubic_bezier.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/easing_function_cubic_bezier_function_name.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/easing_function_linear.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/easing_function_linear_function_name.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/empty_response.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/font_upload.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/h264_encoder_preset.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/horizontal_align.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_auto.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_auto_asset_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_gif.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_gif_asset_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_jpeg.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_jpeg_asset_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_png.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_png_asset_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_svg.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_spec_svg_asset_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/image_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/input_stream.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/input_stream_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/interpolation.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/mp_4_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/mp_4_input_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/opus_encoder_preset.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/output_end_condition.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/output_rtmp_client_audio_options.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/output_rtmp_client_video_options.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/output_whip_audio_options.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/output_whip_video_options.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/overflow.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/pixel_format.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/register_font_body.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/register_input_response.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/register_template_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/register_template_output_body.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rescale_mode.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rescaler.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rescaler_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/resolution.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rtmp_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rtmp_input_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rtmp_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/rtmp_output_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/send_composition_event_body.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/text.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/text_style.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/text_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/text_weight.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/text_wrap_mode.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/tiles.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/tiles_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/transition.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/transport_protocol.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/unregister_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/unregister_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/unregister_renderer.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/update_output_request.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/vertical_align.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/video_scene.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/view.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/view_direction.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/view_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whep_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whep_input_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_audio_encoder_options_any.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_audio_encoder_options_any_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_audio_encoder_options_opus.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_audio_encoder_options_opus_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_input.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_input_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_output.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/models/whip_output_type.py (100%) rename fishjam/{_composition_client => _composition_openapi_client}/types.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/credentials/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/credentials/validate_credentials.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/mo_q/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/mo_q/create_moq_access.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/recordings/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/recordings/create_recording.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/recordings/delete_recording.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/recordings/get_recording.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/recordings/list_recordings.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/recordings/stop_recording.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/add_peer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/create_room.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/delete_peer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/delete_room.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/get_all_rooms.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/get_room.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/refresh_token.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/subscribe_peer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/rooms/subscribe_tracks.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streamers/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streamers/create_streamer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streamers/delete_streamer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streamers/generate_streamer_token.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streams/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streams/create_stream.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streams/delete_stream.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streams/get_all_streams.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/streams/get_stream.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/track_forwardings/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/track_forwardings/create_track_forwarding.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/viewers/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/viewers/create_viewer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/viewers/delete_viewer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/api/viewers/generate_viewer_token.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/client.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/errors.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/__init__.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/agent_output.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/audio_format.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/audio_sample_rate.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/composition_info.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/composition_source.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/error.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/list_recordings_metadata.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/moq_access.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/moq_access_config.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_config_agent.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_config_agent_type.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_config_vapi.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_config_vapi_type.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_config_web_rtc.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_config_web_rtc_type.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_details_response_data.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_metadata.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_options_agent.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_options_vapi.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_options_web_rtc.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_refresh_token_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_refresh_token_response_data.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_status.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/peer_type.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_config.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_config_metadata_type_0.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_file.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_list_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_metadata_type_0.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/recording_status.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/room.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/room_config.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/room_create_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/room_create_details_response_data.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/room_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/room_type.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/rooms_listing_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/stream.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/stream_config.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/stream_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/streamer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/streamer_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/streamer_status.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/streamer_token.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/streams_listing_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/subscribe_mode.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/subscribe_tracks_body.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/subscriptions.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/track.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/track_forwarding.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/track_forwarding_info.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/track_metadata.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/track_type.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/video_codec.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/viewer.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/viewer_details_response.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/viewer_token.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/models/web_rtc_metadata.py (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/py.typed (100%) rename fishjam/{_openapi_client => _fishjam_openapi_client}/types.py (100%) diff --git a/examples/room_manager/room_service.py b/examples/room_manager/room_service.py index 1cb66dc..e2fe6b7 100644 --- a/examples/room_manager/room_service.py +++ b/examples/room_manager/room_service.py @@ -6,7 +6,7 @@ import betterproto from fishjam import FishjamClient, PeerOptions, Room, RoomOptions -from fishjam._openapi_client.models import RoomType +from fishjam._fishjam_openapi_client.models import RoomType from fishjam.events import ServerMessagePeerCrashed as PeerCrashed from fishjam.events import ServerMessagePeerDeleted as PeerDeleted from fishjam.events import ServerMessageRoomCrashed as RoomCrashed diff --git a/fishjam/__init__.py b/fishjam/__init__.py index 99002a5..7703f64 100644 --- a/fishjam/__init__.py +++ b/fishjam/__init__.py @@ -19,7 +19,7 @@ room, version, ) -from fishjam._openapi_client.models import PeerMetadata +from fishjam._fishjam_openapi_client.models import PeerMetadata # API from fishjam._webhook_notifier import ( diff --git a/fishjam/_composition_client/__init__.py b/fishjam/_composition_openapi_client/__init__.py similarity index 100% rename from fishjam/_composition_client/__init__.py rename to fishjam/_composition_openapi_client/__init__.py diff --git a/fishjam/_composition_client/api/__init__.py b/fishjam/_composition_openapi_client/api/__init__.py similarity index 100% rename from fishjam/_composition_client/api/__init__.py rename to fishjam/_composition_openapi_client/api/__init__.py diff --git a/fishjam/_composition_client/api/compositions/__init__.py b/fishjam/_composition_openapi_client/api/compositions/__init__.py similarity index 100% rename from fishjam/_composition_client/api/compositions/__init__.py rename to fishjam/_composition_openapi_client/api/compositions/__init__.py diff --git a/fishjam/_composition_client/api/compositions/create_composition.py b/fishjam/_composition_openapi_client/api/compositions/create_composition.py similarity index 100% rename from fishjam/_composition_client/api/compositions/create_composition.py rename to fishjam/_composition_openapi_client/api/compositions/create_composition.py diff --git a/fishjam/_composition_client/api/compositions/delete_composition.py b/fishjam/_composition_openapi_client/api/compositions/delete_composition.py similarity index 100% rename from fishjam/_composition_client/api/compositions/delete_composition.py rename to fishjam/_composition_openapi_client/api/compositions/delete_composition.py diff --git a/fishjam/_composition_client/api/compositions/reset.py b/fishjam/_composition_openapi_client/api/compositions/reset.py similarity index 100% rename from fishjam/_composition_client/api/compositions/reset.py rename to fishjam/_composition_openapi_client/api/compositions/reset.py diff --git a/fishjam/_composition_client/api/compositions/start.py b/fishjam/_composition_openapi_client/api/compositions/start.py similarity index 100% rename from fishjam/_composition_client/api/compositions/start.py rename to fishjam/_composition_openapi_client/api/compositions/start.py diff --git a/fishjam/_composition_client/api/events/__init__.py b/fishjam/_composition_openapi_client/api/events/__init__.py similarity index 100% rename from fishjam/_composition_client/api/events/__init__.py rename to fishjam/_composition_openapi_client/api/events/__init__.py diff --git a/fishjam/_composition_client/api/events/send_composition_event.py b/fishjam/_composition_openapi_client/api/events/send_composition_event.py similarity index 100% rename from fishjam/_composition_client/api/events/send_composition_event.py rename to fishjam/_composition_openapi_client/api/events/send_composition_event.py diff --git a/fishjam/_composition_client/api/inputs/__init__.py b/fishjam/_composition_openapi_client/api/inputs/__init__.py similarity index 100% rename from fishjam/_composition_client/api/inputs/__init__.py rename to fishjam/_composition_openapi_client/api/inputs/__init__.py diff --git a/fishjam/_composition_client/api/inputs/register_input.py b/fishjam/_composition_openapi_client/api/inputs/register_input.py similarity index 100% rename from fishjam/_composition_client/api/inputs/register_input.py rename to fishjam/_composition_openapi_client/api/inputs/register_input.py diff --git a/fishjam/_composition_client/api/inputs/unregister_input.py b/fishjam/_composition_openapi_client/api/inputs/unregister_input.py similarity index 100% rename from fishjam/_composition_client/api/inputs/unregister_input.py rename to fishjam/_composition_openapi_client/api/inputs/unregister_input.py diff --git a/fishjam/_composition_client/api/media_transport/__init__.py b/fishjam/_composition_openapi_client/api/media_transport/__init__.py similarity index 100% rename from fishjam/_composition_client/api/media_transport/__init__.py rename to fishjam/_composition_openapi_client/api/media_transport/__init__.py diff --git a/fishjam/_composition_client/api/outputs/__init__.py b/fishjam/_composition_openapi_client/api/outputs/__init__.py similarity index 100% rename from fishjam/_composition_client/api/outputs/__init__.py rename to fishjam/_composition_openapi_client/api/outputs/__init__.py diff --git a/fishjam/_composition_client/api/outputs/register_output.py b/fishjam/_composition_openapi_client/api/outputs/register_output.py similarity index 100% rename from fishjam/_composition_client/api/outputs/register_output.py rename to fishjam/_composition_openapi_client/api/outputs/register_output.py diff --git a/fishjam/_composition_client/api/outputs/register_template_output.py b/fishjam/_composition_openapi_client/api/outputs/register_template_output.py similarity index 100% rename from fishjam/_composition_client/api/outputs/register_template_output.py rename to fishjam/_composition_openapi_client/api/outputs/register_template_output.py diff --git a/fishjam/_composition_client/api/outputs/request_keyframe.py b/fishjam/_composition_openapi_client/api/outputs/request_keyframe.py similarity index 100% rename from fishjam/_composition_client/api/outputs/request_keyframe.py rename to fishjam/_composition_openapi_client/api/outputs/request_keyframe.py diff --git a/fishjam/_composition_client/api/outputs/unregister_output.py b/fishjam/_composition_openapi_client/api/outputs/unregister_output.py similarity index 100% rename from fishjam/_composition_client/api/outputs/unregister_output.py rename to fishjam/_composition_openapi_client/api/outputs/unregister_output.py diff --git a/fishjam/_composition_client/api/outputs/update_output.py b/fishjam/_composition_openapi_client/api/outputs/update_output.py similarity index 100% rename from fishjam/_composition_client/api/outputs/update_output.py rename to fishjam/_composition_openapi_client/api/outputs/update_output.py diff --git a/fishjam/_composition_client/api/renderers/__init__.py b/fishjam/_composition_openapi_client/api/renderers/__init__.py similarity index 100% rename from fishjam/_composition_client/api/renderers/__init__.py rename to fishjam/_composition_openapi_client/api/renderers/__init__.py diff --git a/fishjam/_composition_client/api/renderers/register_font.py b/fishjam/_composition_openapi_client/api/renderers/register_font.py similarity index 100% rename from fishjam/_composition_client/api/renderers/register_font.py rename to fishjam/_composition_openapi_client/api/renderers/register_font.py diff --git a/fishjam/_composition_client/api/renderers/register_image.py b/fishjam/_composition_openapi_client/api/renderers/register_image.py similarity index 100% rename from fishjam/_composition_client/api/renderers/register_image.py rename to fishjam/_composition_openapi_client/api/renderers/register_image.py diff --git a/fishjam/_composition_client/api/renderers/unregister_image.py b/fishjam/_composition_openapi_client/api/renderers/unregister_image.py similarity index 100% rename from fishjam/_composition_client/api/renderers/unregister_image.py rename to fishjam/_composition_openapi_client/api/renderers/unregister_image.py diff --git a/fishjam/_composition_client/client.py b/fishjam/_composition_openapi_client/client.py similarity index 100% rename from fishjam/_composition_client/client.py rename to fishjam/_composition_openapi_client/client.py diff --git a/fishjam/_composition_client/errors.py b/fishjam/_composition_openapi_client/errors.py similarity index 100% rename from fishjam/_composition_client/errors.py rename to fishjam/_composition_openapi_client/errors.py diff --git a/fishjam/_composition_client/models/__init__.py b/fishjam/_composition_openapi_client/models/__init__.py similarity index 100% rename from fishjam/_composition_client/models/__init__.py rename to fishjam/_composition_openapi_client/models/__init__.py diff --git a/fishjam/_composition_client/models/api_error.py b/fishjam/_composition_openapi_client/models/api_error.py similarity index 100% rename from fishjam/_composition_client/models/api_error.py rename to fishjam/_composition_openapi_client/models/api_error.py diff --git a/fishjam/_composition_client/models/audio_channels.py b/fishjam/_composition_openapi_client/models/audio_channels.py similarity index 100% rename from fishjam/_composition_client/models/audio_channels.py rename to fishjam/_composition_openapi_client/models/audio_channels.py diff --git a/fishjam/_composition_client/models/audio_mixing_strategy.py b/fishjam/_composition_openapi_client/models/audio_mixing_strategy.py similarity index 100% rename from fishjam/_composition_client/models/audio_mixing_strategy.py rename to fishjam/_composition_openapi_client/models/audio_mixing_strategy.py diff --git a/fishjam/_composition_client/models/audio_scene.py b/fishjam/_composition_openapi_client/models/audio_scene.py similarity index 100% rename from fishjam/_composition_client/models/audio_scene.py rename to fishjam/_composition_openapi_client/models/audio_scene.py diff --git a/fishjam/_composition_client/models/audio_scene_input.py b/fishjam/_composition_openapi_client/models/audio_scene_input.py similarity index 100% rename from fishjam/_composition_client/models/audio_scene_input.py rename to fishjam/_composition_openapi_client/models/audio_scene_input.py diff --git a/fishjam/_composition_client/models/average_and_max_bitrate.py b/fishjam/_composition_openapi_client/models/average_and_max_bitrate.py similarity index 100% rename from fishjam/_composition_client/models/average_and_max_bitrate.py rename to fishjam/_composition_openapi_client/models/average_and_max_bitrate.py diff --git a/fishjam/_composition_client/models/box_shadow.py b/fishjam/_composition_openapi_client/models/box_shadow.py similarity index 100% rename from fishjam/_composition_client/models/box_shadow.py rename to fishjam/_composition_openapi_client/models/box_shadow.py diff --git a/fishjam/_composition_client/models/composition_created_response.py b/fishjam/_composition_openapi_client/models/composition_created_response.py similarity index 100% rename from fishjam/_composition_client/models/composition_created_response.py rename to fishjam/_composition_openapi_client/models/composition_created_response.py diff --git a/fishjam/_composition_client/models/create_composition_request.py b/fishjam/_composition_openapi_client/models/create_composition_request.py similarity index 100% rename from fishjam/_composition_client/models/create_composition_request.py rename to fishjam/_composition_openapi_client/models/create_composition_request.py diff --git a/fishjam/_composition_client/models/easing_function_bounce.py b/fishjam/_composition_openapi_client/models/easing_function_bounce.py similarity index 100% rename from fishjam/_composition_client/models/easing_function_bounce.py rename to fishjam/_composition_openapi_client/models/easing_function_bounce.py diff --git a/fishjam/_composition_client/models/easing_function_bounce_function_name.py b/fishjam/_composition_openapi_client/models/easing_function_bounce_function_name.py similarity index 100% rename from fishjam/_composition_client/models/easing_function_bounce_function_name.py rename to fishjam/_composition_openapi_client/models/easing_function_bounce_function_name.py diff --git a/fishjam/_composition_client/models/easing_function_cubic_bezier.py b/fishjam/_composition_openapi_client/models/easing_function_cubic_bezier.py similarity index 100% rename from fishjam/_composition_client/models/easing_function_cubic_bezier.py rename to fishjam/_composition_openapi_client/models/easing_function_cubic_bezier.py diff --git a/fishjam/_composition_client/models/easing_function_cubic_bezier_function_name.py b/fishjam/_composition_openapi_client/models/easing_function_cubic_bezier_function_name.py similarity index 100% rename from fishjam/_composition_client/models/easing_function_cubic_bezier_function_name.py rename to fishjam/_composition_openapi_client/models/easing_function_cubic_bezier_function_name.py diff --git a/fishjam/_composition_client/models/easing_function_linear.py b/fishjam/_composition_openapi_client/models/easing_function_linear.py similarity index 100% rename from fishjam/_composition_client/models/easing_function_linear.py rename to fishjam/_composition_openapi_client/models/easing_function_linear.py diff --git a/fishjam/_composition_client/models/easing_function_linear_function_name.py b/fishjam/_composition_openapi_client/models/easing_function_linear_function_name.py similarity index 100% rename from fishjam/_composition_client/models/easing_function_linear_function_name.py rename to fishjam/_composition_openapi_client/models/easing_function_linear_function_name.py diff --git a/fishjam/_composition_client/models/empty_response.py b/fishjam/_composition_openapi_client/models/empty_response.py similarity index 100% rename from fishjam/_composition_client/models/empty_response.py rename to fishjam/_composition_openapi_client/models/empty_response.py diff --git a/fishjam/_composition_client/models/font_upload.py b/fishjam/_composition_openapi_client/models/font_upload.py similarity index 100% rename from fishjam/_composition_client/models/font_upload.py rename to fishjam/_composition_openapi_client/models/font_upload.py diff --git a/fishjam/_composition_client/models/h264_encoder_preset.py b/fishjam/_composition_openapi_client/models/h264_encoder_preset.py similarity index 100% rename from fishjam/_composition_client/models/h264_encoder_preset.py rename to fishjam/_composition_openapi_client/models/h264_encoder_preset.py diff --git a/fishjam/_composition_client/models/horizontal_align.py b/fishjam/_composition_openapi_client/models/horizontal_align.py similarity index 100% rename from fishjam/_composition_client/models/horizontal_align.py rename to fishjam/_composition_openapi_client/models/horizontal_align.py diff --git a/fishjam/_composition_client/models/image.py b/fishjam/_composition_openapi_client/models/image.py similarity index 100% rename from fishjam/_composition_client/models/image.py rename to fishjam/_composition_openapi_client/models/image.py diff --git a/fishjam/_composition_client/models/image_spec_auto.py b/fishjam/_composition_openapi_client/models/image_spec_auto.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_auto.py rename to fishjam/_composition_openapi_client/models/image_spec_auto.py diff --git a/fishjam/_composition_client/models/image_spec_auto_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_auto_asset_type.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_auto_asset_type.py rename to fishjam/_composition_openapi_client/models/image_spec_auto_asset_type.py diff --git a/fishjam/_composition_client/models/image_spec_gif.py b/fishjam/_composition_openapi_client/models/image_spec_gif.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_gif.py rename to fishjam/_composition_openapi_client/models/image_spec_gif.py diff --git a/fishjam/_composition_client/models/image_spec_gif_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_gif_asset_type.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_gif_asset_type.py rename to fishjam/_composition_openapi_client/models/image_spec_gif_asset_type.py diff --git a/fishjam/_composition_client/models/image_spec_jpeg.py b/fishjam/_composition_openapi_client/models/image_spec_jpeg.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_jpeg.py rename to fishjam/_composition_openapi_client/models/image_spec_jpeg.py diff --git a/fishjam/_composition_client/models/image_spec_jpeg_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_jpeg_asset_type.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_jpeg_asset_type.py rename to fishjam/_composition_openapi_client/models/image_spec_jpeg_asset_type.py diff --git a/fishjam/_composition_client/models/image_spec_png.py b/fishjam/_composition_openapi_client/models/image_spec_png.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_png.py rename to fishjam/_composition_openapi_client/models/image_spec_png.py diff --git a/fishjam/_composition_client/models/image_spec_png_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_png_asset_type.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_png_asset_type.py rename to fishjam/_composition_openapi_client/models/image_spec_png_asset_type.py diff --git a/fishjam/_composition_client/models/image_spec_svg.py b/fishjam/_composition_openapi_client/models/image_spec_svg.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_svg.py rename to fishjam/_composition_openapi_client/models/image_spec_svg.py diff --git a/fishjam/_composition_client/models/image_spec_svg_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_svg_asset_type.py similarity index 100% rename from fishjam/_composition_client/models/image_spec_svg_asset_type.py rename to fishjam/_composition_openapi_client/models/image_spec_svg_asset_type.py diff --git a/fishjam/_composition_client/models/image_type.py b/fishjam/_composition_openapi_client/models/image_type.py similarity index 100% rename from fishjam/_composition_client/models/image_type.py rename to fishjam/_composition_openapi_client/models/image_type.py diff --git a/fishjam/_composition_client/models/input_stream.py b/fishjam/_composition_openapi_client/models/input_stream.py similarity index 100% rename from fishjam/_composition_client/models/input_stream.py rename to fishjam/_composition_openapi_client/models/input_stream.py diff --git a/fishjam/_composition_client/models/input_stream_type.py b/fishjam/_composition_openapi_client/models/input_stream_type.py similarity index 100% rename from fishjam/_composition_client/models/input_stream_type.py rename to fishjam/_composition_openapi_client/models/input_stream_type.py diff --git a/fishjam/_composition_client/models/interpolation.py b/fishjam/_composition_openapi_client/models/interpolation.py similarity index 100% rename from fishjam/_composition_client/models/interpolation.py rename to fishjam/_composition_openapi_client/models/interpolation.py diff --git a/fishjam/_composition_client/models/mp_4_input.py b/fishjam/_composition_openapi_client/models/mp_4_input.py similarity index 100% rename from fishjam/_composition_client/models/mp_4_input.py rename to fishjam/_composition_openapi_client/models/mp_4_input.py diff --git a/fishjam/_composition_client/models/mp_4_input_type.py b/fishjam/_composition_openapi_client/models/mp_4_input_type.py similarity index 100% rename from fishjam/_composition_client/models/mp_4_input_type.py rename to fishjam/_composition_openapi_client/models/mp_4_input_type.py diff --git a/fishjam/_composition_client/models/opus_encoder_preset.py b/fishjam/_composition_openapi_client/models/opus_encoder_preset.py similarity index 100% rename from fishjam/_composition_client/models/opus_encoder_preset.py rename to fishjam/_composition_openapi_client/models/opus_encoder_preset.py diff --git a/fishjam/_composition_client/models/output_end_condition.py b/fishjam/_composition_openapi_client/models/output_end_condition.py similarity index 100% rename from fishjam/_composition_client/models/output_end_condition.py rename to fishjam/_composition_openapi_client/models/output_end_condition.py diff --git a/fishjam/_composition_client/models/output_rtmp_client_audio_options.py b/fishjam/_composition_openapi_client/models/output_rtmp_client_audio_options.py similarity index 100% rename from fishjam/_composition_client/models/output_rtmp_client_audio_options.py rename to fishjam/_composition_openapi_client/models/output_rtmp_client_audio_options.py diff --git a/fishjam/_composition_client/models/output_rtmp_client_video_options.py b/fishjam/_composition_openapi_client/models/output_rtmp_client_video_options.py similarity index 100% rename from fishjam/_composition_client/models/output_rtmp_client_video_options.py rename to fishjam/_composition_openapi_client/models/output_rtmp_client_video_options.py diff --git a/fishjam/_composition_client/models/output_whip_audio_options.py b/fishjam/_composition_openapi_client/models/output_whip_audio_options.py similarity index 100% rename from fishjam/_composition_client/models/output_whip_audio_options.py rename to fishjam/_composition_openapi_client/models/output_whip_audio_options.py diff --git a/fishjam/_composition_client/models/output_whip_video_options.py b/fishjam/_composition_openapi_client/models/output_whip_video_options.py similarity index 100% rename from fishjam/_composition_client/models/output_whip_video_options.py rename to fishjam/_composition_openapi_client/models/output_whip_video_options.py diff --git a/fishjam/_composition_client/models/overflow.py b/fishjam/_composition_openapi_client/models/overflow.py similarity index 100% rename from fishjam/_composition_client/models/overflow.py rename to fishjam/_composition_openapi_client/models/overflow.py diff --git a/fishjam/_composition_client/models/pixel_format.py b/fishjam/_composition_openapi_client/models/pixel_format.py similarity index 100% rename from fishjam/_composition_client/models/pixel_format.py rename to fishjam/_composition_openapi_client/models/pixel_format.py diff --git a/fishjam/_composition_client/models/register_font_body.py b/fishjam/_composition_openapi_client/models/register_font_body.py similarity index 100% rename from fishjam/_composition_client/models/register_font_body.py rename to fishjam/_composition_openapi_client/models/register_font_body.py diff --git a/fishjam/_composition_client/models/register_input_response.py b/fishjam/_composition_openapi_client/models/register_input_response.py similarity index 100% rename from fishjam/_composition_client/models/register_input_response.py rename to fishjam/_composition_openapi_client/models/register_input_response.py diff --git a/fishjam/_composition_client/models/register_template_output.py b/fishjam/_composition_openapi_client/models/register_template_output.py similarity index 100% rename from fishjam/_composition_client/models/register_template_output.py rename to fishjam/_composition_openapi_client/models/register_template_output.py diff --git a/fishjam/_composition_client/models/register_template_output_body.py b/fishjam/_composition_openapi_client/models/register_template_output_body.py similarity index 100% rename from fishjam/_composition_client/models/register_template_output_body.py rename to fishjam/_composition_openapi_client/models/register_template_output_body.py diff --git a/fishjam/_composition_client/models/rescale_mode.py b/fishjam/_composition_openapi_client/models/rescale_mode.py similarity index 100% rename from fishjam/_composition_client/models/rescale_mode.py rename to fishjam/_composition_openapi_client/models/rescale_mode.py diff --git a/fishjam/_composition_client/models/rescaler.py b/fishjam/_composition_openapi_client/models/rescaler.py similarity index 100% rename from fishjam/_composition_client/models/rescaler.py rename to fishjam/_composition_openapi_client/models/rescaler.py diff --git a/fishjam/_composition_client/models/rescaler_type.py b/fishjam/_composition_openapi_client/models/rescaler_type.py similarity index 100% rename from fishjam/_composition_client/models/rescaler_type.py rename to fishjam/_composition_openapi_client/models/rescaler_type.py diff --git a/fishjam/_composition_client/models/resolution.py b/fishjam/_composition_openapi_client/models/resolution.py similarity index 100% rename from fishjam/_composition_client/models/resolution.py rename to fishjam/_composition_openapi_client/models/resolution.py diff --git a/fishjam/_composition_client/models/rtmp_input.py b/fishjam/_composition_openapi_client/models/rtmp_input.py similarity index 100% rename from fishjam/_composition_client/models/rtmp_input.py rename to fishjam/_composition_openapi_client/models/rtmp_input.py diff --git a/fishjam/_composition_client/models/rtmp_input_type.py b/fishjam/_composition_openapi_client/models/rtmp_input_type.py similarity index 100% rename from fishjam/_composition_client/models/rtmp_input_type.py rename to fishjam/_composition_openapi_client/models/rtmp_input_type.py diff --git a/fishjam/_composition_client/models/rtmp_output.py b/fishjam/_composition_openapi_client/models/rtmp_output.py similarity index 100% rename from fishjam/_composition_client/models/rtmp_output.py rename to fishjam/_composition_openapi_client/models/rtmp_output.py diff --git a/fishjam/_composition_client/models/rtmp_output_type.py b/fishjam/_composition_openapi_client/models/rtmp_output_type.py similarity index 100% rename from fishjam/_composition_client/models/rtmp_output_type.py rename to fishjam/_composition_openapi_client/models/rtmp_output_type.py diff --git a/fishjam/_composition_client/models/send_composition_event_body.py b/fishjam/_composition_openapi_client/models/send_composition_event_body.py similarity index 100% rename from fishjam/_composition_client/models/send_composition_event_body.py rename to fishjam/_composition_openapi_client/models/send_composition_event_body.py diff --git a/fishjam/_composition_client/models/text.py b/fishjam/_composition_openapi_client/models/text.py similarity index 100% rename from fishjam/_composition_client/models/text.py rename to fishjam/_composition_openapi_client/models/text.py diff --git a/fishjam/_composition_client/models/text_style.py b/fishjam/_composition_openapi_client/models/text_style.py similarity index 100% rename from fishjam/_composition_client/models/text_style.py rename to fishjam/_composition_openapi_client/models/text_style.py diff --git a/fishjam/_composition_client/models/text_type.py b/fishjam/_composition_openapi_client/models/text_type.py similarity index 100% rename from fishjam/_composition_client/models/text_type.py rename to fishjam/_composition_openapi_client/models/text_type.py diff --git a/fishjam/_composition_client/models/text_weight.py b/fishjam/_composition_openapi_client/models/text_weight.py similarity index 100% rename from fishjam/_composition_client/models/text_weight.py rename to fishjam/_composition_openapi_client/models/text_weight.py diff --git a/fishjam/_composition_client/models/text_wrap_mode.py b/fishjam/_composition_openapi_client/models/text_wrap_mode.py similarity index 100% rename from fishjam/_composition_client/models/text_wrap_mode.py rename to fishjam/_composition_openapi_client/models/text_wrap_mode.py diff --git a/fishjam/_composition_client/models/tiles.py b/fishjam/_composition_openapi_client/models/tiles.py similarity index 100% rename from fishjam/_composition_client/models/tiles.py rename to fishjam/_composition_openapi_client/models/tiles.py diff --git a/fishjam/_composition_client/models/tiles_type.py b/fishjam/_composition_openapi_client/models/tiles_type.py similarity index 100% rename from fishjam/_composition_client/models/tiles_type.py rename to fishjam/_composition_openapi_client/models/tiles_type.py diff --git a/fishjam/_composition_client/models/transition.py b/fishjam/_composition_openapi_client/models/transition.py similarity index 100% rename from fishjam/_composition_client/models/transition.py rename to fishjam/_composition_openapi_client/models/transition.py diff --git a/fishjam/_composition_client/models/transport_protocol.py b/fishjam/_composition_openapi_client/models/transport_protocol.py similarity index 100% rename from fishjam/_composition_client/models/transport_protocol.py rename to fishjam/_composition_openapi_client/models/transport_protocol.py diff --git a/fishjam/_composition_client/models/unregister_input.py b/fishjam/_composition_openapi_client/models/unregister_input.py similarity index 100% rename from fishjam/_composition_client/models/unregister_input.py rename to fishjam/_composition_openapi_client/models/unregister_input.py diff --git a/fishjam/_composition_client/models/unregister_output.py b/fishjam/_composition_openapi_client/models/unregister_output.py similarity index 100% rename from fishjam/_composition_client/models/unregister_output.py rename to fishjam/_composition_openapi_client/models/unregister_output.py diff --git a/fishjam/_composition_client/models/unregister_renderer.py b/fishjam/_composition_openapi_client/models/unregister_renderer.py similarity index 100% rename from fishjam/_composition_client/models/unregister_renderer.py rename to fishjam/_composition_openapi_client/models/unregister_renderer.py diff --git a/fishjam/_composition_client/models/update_output_request.py b/fishjam/_composition_openapi_client/models/update_output_request.py similarity index 100% rename from fishjam/_composition_client/models/update_output_request.py rename to fishjam/_composition_openapi_client/models/update_output_request.py diff --git a/fishjam/_composition_client/models/vertical_align.py b/fishjam/_composition_openapi_client/models/vertical_align.py similarity index 100% rename from fishjam/_composition_client/models/vertical_align.py rename to fishjam/_composition_openapi_client/models/vertical_align.py diff --git a/fishjam/_composition_client/models/video_scene.py b/fishjam/_composition_openapi_client/models/video_scene.py similarity index 100% rename from fishjam/_composition_client/models/video_scene.py rename to fishjam/_composition_openapi_client/models/video_scene.py diff --git a/fishjam/_composition_client/models/view.py b/fishjam/_composition_openapi_client/models/view.py similarity index 100% rename from fishjam/_composition_client/models/view.py rename to fishjam/_composition_openapi_client/models/view.py diff --git a/fishjam/_composition_client/models/view_direction.py b/fishjam/_composition_openapi_client/models/view_direction.py similarity index 100% rename from fishjam/_composition_client/models/view_direction.py rename to fishjam/_composition_openapi_client/models/view_direction.py diff --git a/fishjam/_composition_client/models/view_type.py b/fishjam/_composition_openapi_client/models/view_type.py similarity index 100% rename from fishjam/_composition_client/models/view_type.py rename to fishjam/_composition_openapi_client/models/view_type.py diff --git a/fishjam/_composition_client/models/whep_input.py b/fishjam/_composition_openapi_client/models/whep_input.py similarity index 100% rename from fishjam/_composition_client/models/whep_input.py rename to fishjam/_composition_openapi_client/models/whep_input.py diff --git a/fishjam/_composition_client/models/whep_input_type.py b/fishjam/_composition_openapi_client/models/whep_input_type.py similarity index 100% rename from fishjam/_composition_client/models/whep_input_type.py rename to fishjam/_composition_openapi_client/models/whep_input_type.py diff --git a/fishjam/_composition_client/models/whip_audio_encoder_options_any.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_any.py similarity index 100% rename from fishjam/_composition_client/models/whip_audio_encoder_options_any.py rename to fishjam/_composition_openapi_client/models/whip_audio_encoder_options_any.py diff --git a/fishjam/_composition_client/models/whip_audio_encoder_options_any_type.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_any_type.py similarity index 100% rename from fishjam/_composition_client/models/whip_audio_encoder_options_any_type.py rename to fishjam/_composition_openapi_client/models/whip_audio_encoder_options_any_type.py diff --git a/fishjam/_composition_client/models/whip_audio_encoder_options_opus.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_opus.py similarity index 100% rename from fishjam/_composition_client/models/whip_audio_encoder_options_opus.py rename to fishjam/_composition_openapi_client/models/whip_audio_encoder_options_opus.py diff --git a/fishjam/_composition_client/models/whip_audio_encoder_options_opus_type.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_opus_type.py similarity index 100% rename from fishjam/_composition_client/models/whip_audio_encoder_options_opus_type.py rename to fishjam/_composition_openapi_client/models/whip_audio_encoder_options_opus_type.py diff --git a/fishjam/_composition_client/models/whip_input.py b/fishjam/_composition_openapi_client/models/whip_input.py similarity index 100% rename from fishjam/_composition_client/models/whip_input.py rename to fishjam/_composition_openapi_client/models/whip_input.py diff --git a/fishjam/_composition_client/models/whip_input_type.py b/fishjam/_composition_openapi_client/models/whip_input_type.py similarity index 100% rename from fishjam/_composition_client/models/whip_input_type.py rename to fishjam/_composition_openapi_client/models/whip_input_type.py diff --git a/fishjam/_composition_client/models/whip_output.py b/fishjam/_composition_openapi_client/models/whip_output.py similarity index 100% rename from fishjam/_composition_client/models/whip_output.py rename to fishjam/_composition_openapi_client/models/whip_output.py diff --git a/fishjam/_composition_client/models/whip_output_type.py b/fishjam/_composition_openapi_client/models/whip_output_type.py similarity index 100% rename from fishjam/_composition_client/models/whip_output_type.py rename to fishjam/_composition_openapi_client/models/whip_output_type.py diff --git a/fishjam/_composition_client/types.py b/fishjam/_composition_openapi_client/types.py similarity index 100% rename from fishjam/_composition_client/types.py rename to fishjam/_composition_openapi_client/types.py diff --git a/fishjam/_openapi_client/__init__.py b/fishjam/_fishjam_openapi_client/__init__.py similarity index 100% rename from fishjam/_openapi_client/__init__.py rename to fishjam/_fishjam_openapi_client/__init__.py diff --git a/fishjam/_openapi_client/api/__init__.py b/fishjam/_fishjam_openapi_client/api/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/__init__.py rename to fishjam/_fishjam_openapi_client/api/__init__.py diff --git a/fishjam/_openapi_client/api/credentials/__init__.py b/fishjam/_fishjam_openapi_client/api/credentials/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/credentials/__init__.py rename to fishjam/_fishjam_openapi_client/api/credentials/__init__.py diff --git a/fishjam/_openapi_client/api/credentials/validate_credentials.py b/fishjam/_fishjam_openapi_client/api/credentials/validate_credentials.py similarity index 100% rename from fishjam/_openapi_client/api/credentials/validate_credentials.py rename to fishjam/_fishjam_openapi_client/api/credentials/validate_credentials.py diff --git a/fishjam/_openapi_client/api/mo_q/__init__.py b/fishjam/_fishjam_openapi_client/api/mo_q/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/mo_q/__init__.py rename to fishjam/_fishjam_openapi_client/api/mo_q/__init__.py diff --git a/fishjam/_openapi_client/api/mo_q/create_moq_access.py b/fishjam/_fishjam_openapi_client/api/mo_q/create_moq_access.py similarity index 100% rename from fishjam/_openapi_client/api/mo_q/create_moq_access.py rename to fishjam/_fishjam_openapi_client/api/mo_q/create_moq_access.py diff --git a/fishjam/_openapi_client/api/recordings/__init__.py b/fishjam/_fishjam_openapi_client/api/recordings/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/recordings/__init__.py rename to fishjam/_fishjam_openapi_client/api/recordings/__init__.py diff --git a/fishjam/_openapi_client/api/recordings/create_recording.py b/fishjam/_fishjam_openapi_client/api/recordings/create_recording.py similarity index 100% rename from fishjam/_openapi_client/api/recordings/create_recording.py rename to fishjam/_fishjam_openapi_client/api/recordings/create_recording.py diff --git a/fishjam/_openapi_client/api/recordings/delete_recording.py b/fishjam/_fishjam_openapi_client/api/recordings/delete_recording.py similarity index 100% rename from fishjam/_openapi_client/api/recordings/delete_recording.py rename to fishjam/_fishjam_openapi_client/api/recordings/delete_recording.py diff --git a/fishjam/_openapi_client/api/recordings/get_recording.py b/fishjam/_fishjam_openapi_client/api/recordings/get_recording.py similarity index 100% rename from fishjam/_openapi_client/api/recordings/get_recording.py rename to fishjam/_fishjam_openapi_client/api/recordings/get_recording.py diff --git a/fishjam/_openapi_client/api/recordings/list_recordings.py b/fishjam/_fishjam_openapi_client/api/recordings/list_recordings.py similarity index 100% rename from fishjam/_openapi_client/api/recordings/list_recordings.py rename to fishjam/_fishjam_openapi_client/api/recordings/list_recordings.py diff --git a/fishjam/_openapi_client/api/recordings/stop_recording.py b/fishjam/_fishjam_openapi_client/api/recordings/stop_recording.py similarity index 100% rename from fishjam/_openapi_client/api/recordings/stop_recording.py rename to fishjam/_fishjam_openapi_client/api/recordings/stop_recording.py diff --git a/fishjam/_openapi_client/api/rooms/__init__.py b/fishjam/_fishjam_openapi_client/api/rooms/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/__init__.py rename to fishjam/_fishjam_openapi_client/api/rooms/__init__.py diff --git a/fishjam/_openapi_client/api/rooms/add_peer.py b/fishjam/_fishjam_openapi_client/api/rooms/add_peer.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/add_peer.py rename to fishjam/_fishjam_openapi_client/api/rooms/add_peer.py diff --git a/fishjam/_openapi_client/api/rooms/create_room.py b/fishjam/_fishjam_openapi_client/api/rooms/create_room.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/create_room.py rename to fishjam/_fishjam_openapi_client/api/rooms/create_room.py diff --git a/fishjam/_openapi_client/api/rooms/delete_peer.py b/fishjam/_fishjam_openapi_client/api/rooms/delete_peer.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/delete_peer.py rename to fishjam/_fishjam_openapi_client/api/rooms/delete_peer.py diff --git a/fishjam/_openapi_client/api/rooms/delete_room.py b/fishjam/_fishjam_openapi_client/api/rooms/delete_room.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/delete_room.py rename to fishjam/_fishjam_openapi_client/api/rooms/delete_room.py diff --git a/fishjam/_openapi_client/api/rooms/get_all_rooms.py b/fishjam/_fishjam_openapi_client/api/rooms/get_all_rooms.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/get_all_rooms.py rename to fishjam/_fishjam_openapi_client/api/rooms/get_all_rooms.py diff --git a/fishjam/_openapi_client/api/rooms/get_room.py b/fishjam/_fishjam_openapi_client/api/rooms/get_room.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/get_room.py rename to fishjam/_fishjam_openapi_client/api/rooms/get_room.py diff --git a/fishjam/_openapi_client/api/rooms/refresh_token.py b/fishjam/_fishjam_openapi_client/api/rooms/refresh_token.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/refresh_token.py rename to fishjam/_fishjam_openapi_client/api/rooms/refresh_token.py diff --git a/fishjam/_openapi_client/api/rooms/subscribe_peer.py b/fishjam/_fishjam_openapi_client/api/rooms/subscribe_peer.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/subscribe_peer.py rename to fishjam/_fishjam_openapi_client/api/rooms/subscribe_peer.py diff --git a/fishjam/_openapi_client/api/rooms/subscribe_tracks.py b/fishjam/_fishjam_openapi_client/api/rooms/subscribe_tracks.py similarity index 100% rename from fishjam/_openapi_client/api/rooms/subscribe_tracks.py rename to fishjam/_fishjam_openapi_client/api/rooms/subscribe_tracks.py diff --git a/fishjam/_openapi_client/api/streamers/__init__.py b/fishjam/_fishjam_openapi_client/api/streamers/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/streamers/__init__.py rename to fishjam/_fishjam_openapi_client/api/streamers/__init__.py diff --git a/fishjam/_openapi_client/api/streamers/create_streamer.py b/fishjam/_fishjam_openapi_client/api/streamers/create_streamer.py similarity index 100% rename from fishjam/_openapi_client/api/streamers/create_streamer.py rename to fishjam/_fishjam_openapi_client/api/streamers/create_streamer.py diff --git a/fishjam/_openapi_client/api/streamers/delete_streamer.py b/fishjam/_fishjam_openapi_client/api/streamers/delete_streamer.py similarity index 100% rename from fishjam/_openapi_client/api/streamers/delete_streamer.py rename to fishjam/_fishjam_openapi_client/api/streamers/delete_streamer.py diff --git a/fishjam/_openapi_client/api/streamers/generate_streamer_token.py b/fishjam/_fishjam_openapi_client/api/streamers/generate_streamer_token.py similarity index 100% rename from fishjam/_openapi_client/api/streamers/generate_streamer_token.py rename to fishjam/_fishjam_openapi_client/api/streamers/generate_streamer_token.py diff --git a/fishjam/_openapi_client/api/streams/__init__.py b/fishjam/_fishjam_openapi_client/api/streams/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/streams/__init__.py rename to fishjam/_fishjam_openapi_client/api/streams/__init__.py diff --git a/fishjam/_openapi_client/api/streams/create_stream.py b/fishjam/_fishjam_openapi_client/api/streams/create_stream.py similarity index 100% rename from fishjam/_openapi_client/api/streams/create_stream.py rename to fishjam/_fishjam_openapi_client/api/streams/create_stream.py diff --git a/fishjam/_openapi_client/api/streams/delete_stream.py b/fishjam/_fishjam_openapi_client/api/streams/delete_stream.py similarity index 100% rename from fishjam/_openapi_client/api/streams/delete_stream.py rename to fishjam/_fishjam_openapi_client/api/streams/delete_stream.py diff --git a/fishjam/_openapi_client/api/streams/get_all_streams.py b/fishjam/_fishjam_openapi_client/api/streams/get_all_streams.py similarity index 100% rename from fishjam/_openapi_client/api/streams/get_all_streams.py rename to fishjam/_fishjam_openapi_client/api/streams/get_all_streams.py diff --git a/fishjam/_openapi_client/api/streams/get_stream.py b/fishjam/_fishjam_openapi_client/api/streams/get_stream.py similarity index 100% rename from fishjam/_openapi_client/api/streams/get_stream.py rename to fishjam/_fishjam_openapi_client/api/streams/get_stream.py diff --git a/fishjam/_openapi_client/api/track_forwardings/__init__.py b/fishjam/_fishjam_openapi_client/api/track_forwardings/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/track_forwardings/__init__.py rename to fishjam/_fishjam_openapi_client/api/track_forwardings/__init__.py diff --git a/fishjam/_openapi_client/api/track_forwardings/create_track_forwarding.py b/fishjam/_fishjam_openapi_client/api/track_forwardings/create_track_forwarding.py similarity index 100% rename from fishjam/_openapi_client/api/track_forwardings/create_track_forwarding.py rename to fishjam/_fishjam_openapi_client/api/track_forwardings/create_track_forwarding.py diff --git a/fishjam/_openapi_client/api/viewers/__init__.py b/fishjam/_fishjam_openapi_client/api/viewers/__init__.py similarity index 100% rename from fishjam/_openapi_client/api/viewers/__init__.py rename to fishjam/_fishjam_openapi_client/api/viewers/__init__.py diff --git a/fishjam/_openapi_client/api/viewers/create_viewer.py b/fishjam/_fishjam_openapi_client/api/viewers/create_viewer.py similarity index 100% rename from fishjam/_openapi_client/api/viewers/create_viewer.py rename to fishjam/_fishjam_openapi_client/api/viewers/create_viewer.py diff --git a/fishjam/_openapi_client/api/viewers/delete_viewer.py b/fishjam/_fishjam_openapi_client/api/viewers/delete_viewer.py similarity index 100% rename from fishjam/_openapi_client/api/viewers/delete_viewer.py rename to fishjam/_fishjam_openapi_client/api/viewers/delete_viewer.py diff --git a/fishjam/_openapi_client/api/viewers/generate_viewer_token.py b/fishjam/_fishjam_openapi_client/api/viewers/generate_viewer_token.py similarity index 100% rename from fishjam/_openapi_client/api/viewers/generate_viewer_token.py rename to fishjam/_fishjam_openapi_client/api/viewers/generate_viewer_token.py diff --git a/fishjam/_openapi_client/client.py b/fishjam/_fishjam_openapi_client/client.py similarity index 100% rename from fishjam/_openapi_client/client.py rename to fishjam/_fishjam_openapi_client/client.py diff --git a/fishjam/_openapi_client/errors.py b/fishjam/_fishjam_openapi_client/errors.py similarity index 100% rename from fishjam/_openapi_client/errors.py rename to fishjam/_fishjam_openapi_client/errors.py diff --git a/fishjam/_openapi_client/models/__init__.py b/fishjam/_fishjam_openapi_client/models/__init__.py similarity index 100% rename from fishjam/_openapi_client/models/__init__.py rename to fishjam/_fishjam_openapi_client/models/__init__.py diff --git a/fishjam/_openapi_client/models/agent_output.py b/fishjam/_fishjam_openapi_client/models/agent_output.py similarity index 100% rename from fishjam/_openapi_client/models/agent_output.py rename to fishjam/_fishjam_openapi_client/models/agent_output.py diff --git a/fishjam/_openapi_client/models/audio_format.py b/fishjam/_fishjam_openapi_client/models/audio_format.py similarity index 100% rename from fishjam/_openapi_client/models/audio_format.py rename to fishjam/_fishjam_openapi_client/models/audio_format.py diff --git a/fishjam/_openapi_client/models/audio_sample_rate.py b/fishjam/_fishjam_openapi_client/models/audio_sample_rate.py similarity index 100% rename from fishjam/_openapi_client/models/audio_sample_rate.py rename to fishjam/_fishjam_openapi_client/models/audio_sample_rate.py diff --git a/fishjam/_openapi_client/models/composition_info.py b/fishjam/_fishjam_openapi_client/models/composition_info.py similarity index 100% rename from fishjam/_openapi_client/models/composition_info.py rename to fishjam/_fishjam_openapi_client/models/composition_info.py diff --git a/fishjam/_openapi_client/models/composition_source.py b/fishjam/_fishjam_openapi_client/models/composition_source.py similarity index 100% rename from fishjam/_openapi_client/models/composition_source.py rename to fishjam/_fishjam_openapi_client/models/composition_source.py diff --git a/fishjam/_openapi_client/models/error.py b/fishjam/_fishjam_openapi_client/models/error.py similarity index 100% rename from fishjam/_openapi_client/models/error.py rename to fishjam/_fishjam_openapi_client/models/error.py diff --git a/fishjam/_openapi_client/models/list_recordings_metadata.py b/fishjam/_fishjam_openapi_client/models/list_recordings_metadata.py similarity index 100% rename from fishjam/_openapi_client/models/list_recordings_metadata.py rename to fishjam/_fishjam_openapi_client/models/list_recordings_metadata.py diff --git a/fishjam/_openapi_client/models/moq_access.py b/fishjam/_fishjam_openapi_client/models/moq_access.py similarity index 100% rename from fishjam/_openapi_client/models/moq_access.py rename to fishjam/_fishjam_openapi_client/models/moq_access.py diff --git a/fishjam/_openapi_client/models/moq_access_config.py b/fishjam/_fishjam_openapi_client/models/moq_access_config.py similarity index 100% rename from fishjam/_openapi_client/models/moq_access_config.py rename to fishjam/_fishjam_openapi_client/models/moq_access_config.py diff --git a/fishjam/_openapi_client/models/peer.py b/fishjam/_fishjam_openapi_client/models/peer.py similarity index 100% rename from fishjam/_openapi_client/models/peer.py rename to fishjam/_fishjam_openapi_client/models/peer.py diff --git a/fishjam/_openapi_client/models/peer_config_agent.py b/fishjam/_fishjam_openapi_client/models/peer_config_agent.py similarity index 100% rename from fishjam/_openapi_client/models/peer_config_agent.py rename to fishjam/_fishjam_openapi_client/models/peer_config_agent.py diff --git a/fishjam/_openapi_client/models/peer_config_agent_type.py b/fishjam/_fishjam_openapi_client/models/peer_config_agent_type.py similarity index 100% rename from fishjam/_openapi_client/models/peer_config_agent_type.py rename to fishjam/_fishjam_openapi_client/models/peer_config_agent_type.py diff --git a/fishjam/_openapi_client/models/peer_config_vapi.py b/fishjam/_fishjam_openapi_client/models/peer_config_vapi.py similarity index 100% rename from fishjam/_openapi_client/models/peer_config_vapi.py rename to fishjam/_fishjam_openapi_client/models/peer_config_vapi.py diff --git a/fishjam/_openapi_client/models/peer_config_vapi_type.py b/fishjam/_fishjam_openapi_client/models/peer_config_vapi_type.py similarity index 100% rename from fishjam/_openapi_client/models/peer_config_vapi_type.py rename to fishjam/_fishjam_openapi_client/models/peer_config_vapi_type.py diff --git a/fishjam/_openapi_client/models/peer_config_web_rtc.py b/fishjam/_fishjam_openapi_client/models/peer_config_web_rtc.py similarity index 100% rename from fishjam/_openapi_client/models/peer_config_web_rtc.py rename to fishjam/_fishjam_openapi_client/models/peer_config_web_rtc.py diff --git a/fishjam/_openapi_client/models/peer_config_web_rtc_type.py b/fishjam/_fishjam_openapi_client/models/peer_config_web_rtc_type.py similarity index 100% rename from fishjam/_openapi_client/models/peer_config_web_rtc_type.py rename to fishjam/_fishjam_openapi_client/models/peer_config_web_rtc_type.py diff --git a/fishjam/_openapi_client/models/peer_details_response.py b/fishjam/_fishjam_openapi_client/models/peer_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/peer_details_response.py rename to fishjam/_fishjam_openapi_client/models/peer_details_response.py diff --git a/fishjam/_openapi_client/models/peer_details_response_data.py b/fishjam/_fishjam_openapi_client/models/peer_details_response_data.py similarity index 100% rename from fishjam/_openapi_client/models/peer_details_response_data.py rename to fishjam/_fishjam_openapi_client/models/peer_details_response_data.py diff --git a/fishjam/_openapi_client/models/peer_metadata.py b/fishjam/_fishjam_openapi_client/models/peer_metadata.py similarity index 100% rename from fishjam/_openapi_client/models/peer_metadata.py rename to fishjam/_fishjam_openapi_client/models/peer_metadata.py diff --git a/fishjam/_openapi_client/models/peer_options_agent.py b/fishjam/_fishjam_openapi_client/models/peer_options_agent.py similarity index 100% rename from fishjam/_openapi_client/models/peer_options_agent.py rename to fishjam/_fishjam_openapi_client/models/peer_options_agent.py diff --git a/fishjam/_openapi_client/models/peer_options_vapi.py b/fishjam/_fishjam_openapi_client/models/peer_options_vapi.py similarity index 100% rename from fishjam/_openapi_client/models/peer_options_vapi.py rename to fishjam/_fishjam_openapi_client/models/peer_options_vapi.py diff --git a/fishjam/_openapi_client/models/peer_options_web_rtc.py b/fishjam/_fishjam_openapi_client/models/peer_options_web_rtc.py similarity index 100% rename from fishjam/_openapi_client/models/peer_options_web_rtc.py rename to fishjam/_fishjam_openapi_client/models/peer_options_web_rtc.py diff --git a/fishjam/_openapi_client/models/peer_refresh_token_response.py b/fishjam/_fishjam_openapi_client/models/peer_refresh_token_response.py similarity index 100% rename from fishjam/_openapi_client/models/peer_refresh_token_response.py rename to fishjam/_fishjam_openapi_client/models/peer_refresh_token_response.py diff --git a/fishjam/_openapi_client/models/peer_refresh_token_response_data.py b/fishjam/_fishjam_openapi_client/models/peer_refresh_token_response_data.py similarity index 100% rename from fishjam/_openapi_client/models/peer_refresh_token_response_data.py rename to fishjam/_fishjam_openapi_client/models/peer_refresh_token_response_data.py diff --git a/fishjam/_openapi_client/models/peer_status.py b/fishjam/_fishjam_openapi_client/models/peer_status.py similarity index 100% rename from fishjam/_openapi_client/models/peer_status.py rename to fishjam/_fishjam_openapi_client/models/peer_status.py diff --git a/fishjam/_openapi_client/models/peer_type.py b/fishjam/_fishjam_openapi_client/models/peer_type.py similarity index 100% rename from fishjam/_openapi_client/models/peer_type.py rename to fishjam/_fishjam_openapi_client/models/peer_type.py diff --git a/fishjam/_openapi_client/models/recording.py b/fishjam/_fishjam_openapi_client/models/recording.py similarity index 100% rename from fishjam/_openapi_client/models/recording.py rename to fishjam/_fishjam_openapi_client/models/recording.py diff --git a/fishjam/_openapi_client/models/recording_config.py b/fishjam/_fishjam_openapi_client/models/recording_config.py similarity index 100% rename from fishjam/_openapi_client/models/recording_config.py rename to fishjam/_fishjam_openapi_client/models/recording_config.py diff --git a/fishjam/_openapi_client/models/recording_config_metadata_type_0.py b/fishjam/_fishjam_openapi_client/models/recording_config_metadata_type_0.py similarity index 100% rename from fishjam/_openapi_client/models/recording_config_metadata_type_0.py rename to fishjam/_fishjam_openapi_client/models/recording_config_metadata_type_0.py diff --git a/fishjam/_openapi_client/models/recording_details_response.py b/fishjam/_fishjam_openapi_client/models/recording_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/recording_details_response.py rename to fishjam/_fishjam_openapi_client/models/recording_details_response.py diff --git a/fishjam/_openapi_client/models/recording_file.py b/fishjam/_fishjam_openapi_client/models/recording_file.py similarity index 100% rename from fishjam/_openapi_client/models/recording_file.py rename to fishjam/_fishjam_openapi_client/models/recording_file.py diff --git a/fishjam/_openapi_client/models/recording_list_response.py b/fishjam/_fishjam_openapi_client/models/recording_list_response.py similarity index 100% rename from fishjam/_openapi_client/models/recording_list_response.py rename to fishjam/_fishjam_openapi_client/models/recording_list_response.py diff --git a/fishjam/_openapi_client/models/recording_metadata_type_0.py b/fishjam/_fishjam_openapi_client/models/recording_metadata_type_0.py similarity index 100% rename from fishjam/_openapi_client/models/recording_metadata_type_0.py rename to fishjam/_fishjam_openapi_client/models/recording_metadata_type_0.py diff --git a/fishjam/_openapi_client/models/recording_status.py b/fishjam/_fishjam_openapi_client/models/recording_status.py similarity index 100% rename from fishjam/_openapi_client/models/recording_status.py rename to fishjam/_fishjam_openapi_client/models/recording_status.py diff --git a/fishjam/_openapi_client/models/room.py b/fishjam/_fishjam_openapi_client/models/room.py similarity index 100% rename from fishjam/_openapi_client/models/room.py rename to fishjam/_fishjam_openapi_client/models/room.py diff --git a/fishjam/_openapi_client/models/room_config.py b/fishjam/_fishjam_openapi_client/models/room_config.py similarity index 100% rename from fishjam/_openapi_client/models/room_config.py rename to fishjam/_fishjam_openapi_client/models/room_config.py diff --git a/fishjam/_openapi_client/models/room_create_details_response.py b/fishjam/_fishjam_openapi_client/models/room_create_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/room_create_details_response.py rename to fishjam/_fishjam_openapi_client/models/room_create_details_response.py diff --git a/fishjam/_openapi_client/models/room_create_details_response_data.py b/fishjam/_fishjam_openapi_client/models/room_create_details_response_data.py similarity index 100% rename from fishjam/_openapi_client/models/room_create_details_response_data.py rename to fishjam/_fishjam_openapi_client/models/room_create_details_response_data.py diff --git a/fishjam/_openapi_client/models/room_details_response.py b/fishjam/_fishjam_openapi_client/models/room_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/room_details_response.py rename to fishjam/_fishjam_openapi_client/models/room_details_response.py diff --git a/fishjam/_openapi_client/models/room_type.py b/fishjam/_fishjam_openapi_client/models/room_type.py similarity index 100% rename from fishjam/_openapi_client/models/room_type.py rename to fishjam/_fishjam_openapi_client/models/room_type.py diff --git a/fishjam/_openapi_client/models/rooms_listing_response.py b/fishjam/_fishjam_openapi_client/models/rooms_listing_response.py similarity index 100% rename from fishjam/_openapi_client/models/rooms_listing_response.py rename to fishjam/_fishjam_openapi_client/models/rooms_listing_response.py diff --git a/fishjam/_openapi_client/models/stream.py b/fishjam/_fishjam_openapi_client/models/stream.py similarity index 100% rename from fishjam/_openapi_client/models/stream.py rename to fishjam/_fishjam_openapi_client/models/stream.py diff --git a/fishjam/_openapi_client/models/stream_config.py b/fishjam/_fishjam_openapi_client/models/stream_config.py similarity index 100% rename from fishjam/_openapi_client/models/stream_config.py rename to fishjam/_fishjam_openapi_client/models/stream_config.py diff --git a/fishjam/_openapi_client/models/stream_details_response.py b/fishjam/_fishjam_openapi_client/models/stream_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/stream_details_response.py rename to fishjam/_fishjam_openapi_client/models/stream_details_response.py diff --git a/fishjam/_openapi_client/models/streamer.py b/fishjam/_fishjam_openapi_client/models/streamer.py similarity index 100% rename from fishjam/_openapi_client/models/streamer.py rename to fishjam/_fishjam_openapi_client/models/streamer.py diff --git a/fishjam/_openapi_client/models/streamer_details_response.py b/fishjam/_fishjam_openapi_client/models/streamer_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/streamer_details_response.py rename to fishjam/_fishjam_openapi_client/models/streamer_details_response.py diff --git a/fishjam/_openapi_client/models/streamer_status.py b/fishjam/_fishjam_openapi_client/models/streamer_status.py similarity index 100% rename from fishjam/_openapi_client/models/streamer_status.py rename to fishjam/_fishjam_openapi_client/models/streamer_status.py diff --git a/fishjam/_openapi_client/models/streamer_token.py b/fishjam/_fishjam_openapi_client/models/streamer_token.py similarity index 100% rename from fishjam/_openapi_client/models/streamer_token.py rename to fishjam/_fishjam_openapi_client/models/streamer_token.py diff --git a/fishjam/_openapi_client/models/streams_listing_response.py b/fishjam/_fishjam_openapi_client/models/streams_listing_response.py similarity index 100% rename from fishjam/_openapi_client/models/streams_listing_response.py rename to fishjam/_fishjam_openapi_client/models/streams_listing_response.py diff --git a/fishjam/_openapi_client/models/subscribe_mode.py b/fishjam/_fishjam_openapi_client/models/subscribe_mode.py similarity index 100% rename from fishjam/_openapi_client/models/subscribe_mode.py rename to fishjam/_fishjam_openapi_client/models/subscribe_mode.py diff --git a/fishjam/_openapi_client/models/subscribe_tracks_body.py b/fishjam/_fishjam_openapi_client/models/subscribe_tracks_body.py similarity index 100% rename from fishjam/_openapi_client/models/subscribe_tracks_body.py rename to fishjam/_fishjam_openapi_client/models/subscribe_tracks_body.py diff --git a/fishjam/_openapi_client/models/subscriptions.py b/fishjam/_fishjam_openapi_client/models/subscriptions.py similarity index 100% rename from fishjam/_openapi_client/models/subscriptions.py rename to fishjam/_fishjam_openapi_client/models/subscriptions.py diff --git a/fishjam/_openapi_client/models/track.py b/fishjam/_fishjam_openapi_client/models/track.py similarity index 100% rename from fishjam/_openapi_client/models/track.py rename to fishjam/_fishjam_openapi_client/models/track.py diff --git a/fishjam/_openapi_client/models/track_forwarding.py b/fishjam/_fishjam_openapi_client/models/track_forwarding.py similarity index 100% rename from fishjam/_openapi_client/models/track_forwarding.py rename to fishjam/_fishjam_openapi_client/models/track_forwarding.py diff --git a/fishjam/_openapi_client/models/track_forwarding_info.py b/fishjam/_fishjam_openapi_client/models/track_forwarding_info.py similarity index 100% rename from fishjam/_openapi_client/models/track_forwarding_info.py rename to fishjam/_fishjam_openapi_client/models/track_forwarding_info.py diff --git a/fishjam/_openapi_client/models/track_metadata.py b/fishjam/_fishjam_openapi_client/models/track_metadata.py similarity index 100% rename from fishjam/_openapi_client/models/track_metadata.py rename to fishjam/_fishjam_openapi_client/models/track_metadata.py diff --git a/fishjam/_openapi_client/models/track_type.py b/fishjam/_fishjam_openapi_client/models/track_type.py similarity index 100% rename from fishjam/_openapi_client/models/track_type.py rename to fishjam/_fishjam_openapi_client/models/track_type.py diff --git a/fishjam/_openapi_client/models/video_codec.py b/fishjam/_fishjam_openapi_client/models/video_codec.py similarity index 100% rename from fishjam/_openapi_client/models/video_codec.py rename to fishjam/_fishjam_openapi_client/models/video_codec.py diff --git a/fishjam/_openapi_client/models/viewer.py b/fishjam/_fishjam_openapi_client/models/viewer.py similarity index 100% rename from fishjam/_openapi_client/models/viewer.py rename to fishjam/_fishjam_openapi_client/models/viewer.py diff --git a/fishjam/_openapi_client/models/viewer_details_response.py b/fishjam/_fishjam_openapi_client/models/viewer_details_response.py similarity index 100% rename from fishjam/_openapi_client/models/viewer_details_response.py rename to fishjam/_fishjam_openapi_client/models/viewer_details_response.py diff --git a/fishjam/_openapi_client/models/viewer_token.py b/fishjam/_fishjam_openapi_client/models/viewer_token.py similarity index 100% rename from fishjam/_openapi_client/models/viewer_token.py rename to fishjam/_fishjam_openapi_client/models/viewer_token.py diff --git a/fishjam/_openapi_client/models/web_rtc_metadata.py b/fishjam/_fishjam_openapi_client/models/web_rtc_metadata.py similarity index 100% rename from fishjam/_openapi_client/models/web_rtc_metadata.py rename to fishjam/_fishjam_openapi_client/models/web_rtc_metadata.py diff --git a/fishjam/_openapi_client/py.typed b/fishjam/_fishjam_openapi_client/py.typed similarity index 100% rename from fishjam/_openapi_client/py.typed rename to fishjam/_fishjam_openapi_client/py.typed diff --git a/fishjam/_openapi_client/types.py b/fishjam/_fishjam_openapi_client/types.py similarity index 100% rename from fishjam/_openapi_client/types.py rename to fishjam/_fishjam_openapi_client/types.py diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index b2fd203..e11cae5 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -3,9 +3,9 @@ from http import HTTPStatus from typing import cast -from fishjam._openapi_client.client import AuthenticatedClient -from fishjam._openapi_client.models import Error -from fishjam._openapi_client.types import Response +from fishjam._fishjam_openapi_client.client import AuthenticatedClient +from fishjam._fishjam_openapi_client.models import Error +from fishjam._fishjam_openapi_client.types import Response from fishjam.errors import HTTPError from fishjam.utils import get_fishjam_url from fishjam.version import get_version diff --git a/fishjam/api/_composition_client.py b/fishjam/api/_composition_client.py index f0ddb17..4ff256e 100644 --- a/fishjam/api/_composition_client.py +++ b/fishjam/api/_composition_client.py @@ -7,54 +7,54 @@ from typing import Any, TypeVar, cast from urllib.parse import quote -from fishjam._composition_client.api.compositions import ( +from fishjam._composition_openapi_client.api.compositions import ( create_composition as compositions_create, ) -from fishjam._composition_client.api.compositions import ( +from fishjam._composition_openapi_client.api.compositions import ( delete_composition as compositions_delete, ) -from fishjam._composition_client.api.compositions import ( +from fishjam._composition_openapi_client.api.compositions import ( reset as compositions_reset, ) -from fishjam._composition_client.api.compositions import ( +from fishjam._composition_openapi_client.api.compositions import ( start as compositions_start, ) -from fishjam._composition_client.api.events import ( +from fishjam._composition_openapi_client.api.events import ( send_composition_event as events_send, ) -from fishjam._composition_client.api.inputs import ( +from fishjam._composition_openapi_client.api.inputs import ( register_input as inputs_register, ) -from fishjam._composition_client.api.inputs import ( +from fishjam._composition_openapi_client.api.inputs import ( unregister_input as inputs_unregister, ) -from fishjam._composition_client.api.outputs import ( +from fishjam._composition_openapi_client.api.outputs import ( register_output as outputs_register, ) -from fishjam._composition_client.api.outputs import ( +from fishjam._composition_openapi_client.api.outputs import ( register_template_output as outputs_register_template, ) -from fishjam._composition_client.api.outputs import ( +from fishjam._composition_openapi_client.api.outputs import ( request_keyframe as outputs_request_keyframe, ) -from fishjam._composition_client.api.outputs import ( +from fishjam._composition_openapi_client.api.outputs import ( unregister_output as outputs_unregister, ) -from fishjam._composition_client.api.outputs import ( +from fishjam._composition_openapi_client.api.outputs import ( update_output as outputs_update, ) -from fishjam._composition_client.api.renderers import ( +from fishjam._composition_openapi_client.api.renderers import ( register_font as renderers_register_font, ) -from fishjam._composition_client.api.renderers import ( +from fishjam._composition_openapi_client.api.renderers import ( register_image as renderers_register_image, ) -from fishjam._composition_client.api.renderers import ( +from fishjam._composition_openapi_client.api.renderers import ( unregister_image as renderers_unregister_image, ) -from fishjam._composition_client.client import AuthenticatedClient -from fishjam._composition_client.errors import UnexpectedStatus -from fishjam._composition_client.models import ( +from fishjam._composition_openapi_client.client import AuthenticatedClient +from fishjam._composition_openapi_client.errors import UnexpectedStatus +from fishjam._composition_openapi_client.models import ( ApiError, AudioScene, CompositionCreatedResponse, @@ -85,7 +85,7 @@ WhipOutput, WhipOutputType, ) -from fishjam._composition_client.types import UNSET, File, Unset +from fishjam._composition_openapi_client.types import UNSET, File, Unset from fishjam.composition import ( FileSource, ImageSpec, diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index c42b502..44ce2db 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -4,46 +4,54 @@ from http import HTTPStatus from typing import Any, Literal, cast -from fishjam._openapi_client.api.credentials import ( +from fishjam._fishjam_openapi_client.api.credentials import ( validate_credentials as credentials_validate_credentials, ) -from fishjam._openapi_client.api.mo_q import ( +from fishjam._fishjam_openapi_client.api.mo_q import ( create_moq_access as moq_create_access, ) -from fishjam._openapi_client.api.recordings import ( +from fishjam._fishjam_openapi_client.api.recordings import ( create_recording as recording_create_recording, ) -from fishjam._openapi_client.api.recordings import ( +from fishjam._fishjam_openapi_client.api.recordings import ( delete_recording as recording_delete_recording, ) -from fishjam._openapi_client.api.recordings import ( +from fishjam._fishjam_openapi_client.api.recordings import ( get_recording as recording_get_recording, ) -from fishjam._openapi_client.api.recordings import ( +from fishjam._fishjam_openapi_client.api.recordings import ( list_recordings as recording_list_recordings, ) -from fishjam._openapi_client.api.recordings import ( +from fishjam._fishjam_openapi_client.api.recordings import ( stop_recording as recording_stop_recording, ) -from fishjam._openapi_client.api.rooms import add_peer as room_add_peer -from fishjam._openapi_client.api.rooms import create_room as room_create_room -from fishjam._openapi_client.api.rooms import delete_peer as room_delete_peer -from fishjam._openapi_client.api.rooms import delete_room as room_delete_room -from fishjam._openapi_client.api.rooms import get_all_rooms as room_get_all_rooms -from fishjam._openapi_client.api.rooms import get_room as room_get_room -from fishjam._openapi_client.api.rooms import refresh_token as room_refresh_token -from fishjam._openapi_client.api.rooms import subscribe_peer as room_subscribe_peer -from fishjam._openapi_client.api.rooms import subscribe_tracks as room_subscribe_tracks -from fishjam._openapi_client.api.streamers import ( +from fishjam._fishjam_openapi_client.api.rooms import add_peer as room_add_peer +from fishjam._fishjam_openapi_client.api.rooms import create_room as room_create_room +from fishjam._fishjam_openapi_client.api.rooms import delete_peer as room_delete_peer +from fishjam._fishjam_openapi_client.api.rooms import delete_room as room_delete_room +from fishjam._fishjam_openapi_client.api.rooms import ( + get_all_rooms as room_get_all_rooms, +) +from fishjam._fishjam_openapi_client.api.rooms import get_room as room_get_room +from fishjam._fishjam_openapi_client.api.rooms import ( + refresh_token as room_refresh_token, +) +from fishjam._fishjam_openapi_client.api.rooms import ( + subscribe_peer as room_subscribe_peer, +) +from fishjam._fishjam_openapi_client.api.rooms import ( + subscribe_tracks as room_subscribe_tracks, +) +from fishjam._fishjam_openapi_client.api.streamers import ( generate_streamer_token as streamer_generate_streamer_token, ) -from fishjam._openapi_client.api.track_forwardings import ( +from fishjam._fishjam_openapi_client.api.track_forwardings import ( create_track_forwarding as track_forwardings_create, ) -from fishjam._openapi_client.api.viewers import ( +from fishjam._fishjam_openapi_client.api.viewers import ( generate_viewer_token as viewer_generate_viewer_token, ) -from fishjam._openapi_client.models import ( +from fishjam._fishjam_openapi_client.models import ( AgentOutput, AudioFormat, AudioSampleRate, @@ -82,7 +90,7 @@ ViewerToken, WebRTCMetadata, ) -from fishjam._openapi_client.types import UNSET, Unset +from fishjam._fishjam_openapi_client.types import UNSET, Unset from fishjam.agent import Agent from fishjam.api._client import Client from fishjam.errors import ( diff --git a/fishjam/composition/__init__.py b/fishjam/composition/__init__.py index dd2a320..48588f9 100644 --- a/fishjam/composition/__init__.py +++ b/fishjam/composition/__init__.py @@ -1,6 +1,6 @@ from pathlib import Path -from fishjam._composition_client.models import ( +from fishjam._composition_openapi_client.models import ( AudioChannels, AudioMixingStrategy, AudioScene, diff --git a/fishjam/errors.py b/fishjam/errors.py index cb588a1..f752acc 100644 --- a/fishjam/errors.py +++ b/fishjam/errors.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from fishjam._openapi_client.models import Error -from fishjam._openapi_client.types import Response +from fishjam._fishjam_openapi_client.models import Error +from fishjam._fishjam_openapi_client.types import Response class MissingFishjamIdError(ValueError): diff --git a/fishjam/peer/__init__.py b/fishjam/peer/__init__.py index 712fb83..6b22ff9 100644 --- a/fishjam/peer/__init__.py +++ b/fishjam/peer/__init__.py @@ -1,4 +1,4 @@ -from fishjam._openapi_client.models import ( +from fishjam._fishjam_openapi_client.models import ( PeerMetadata, PeerStatus, PeerType, diff --git a/fishjam/recording/__init__.py b/fishjam/recording/__init__.py index 7dc5d3f..c6be474 100644 --- a/fishjam/recording/__init__.py +++ b/fishjam/recording/__init__.py @@ -1,4 +1,4 @@ -from fishjam._openapi_client.models import ( +from fishjam._fishjam_openapi_client.models import ( CompositionSource, Recording, RecordingStatus, diff --git a/fishjam/room/__init__.py b/fishjam/room/__init__.py index fb0a75f..d7b5b8d 100644 --- a/fishjam/room/__init__.py +++ b/fishjam/room/__init__.py @@ -1,4 +1,4 @@ -from fishjam._openapi_client.models import ( +from fishjam._fishjam_openapi_client.models import ( RoomConfig, RoomType, VideoCodec, diff --git a/openapi-python-client-composition-config.yaml b/openapi-python-client-composition-config.yaml index 665c33c..57c33ab 100644 --- a/openapi-python-client-composition-config.yaml +++ b/openapi-python-client-composition-config.yaml @@ -1,2 +1,2 @@ project_name_override: fishjam -package_name_override: _composition_client +package_name_override: _composition_openapi_client diff --git a/openapi-python-client-config.yaml b/openapi-python-client-config.yaml index 1bfe57c..8d78abf 100644 --- a/openapi-python-client-config.yaml +++ b/openapi-python-client-config.yaml @@ -1,2 +1,2 @@ project_name_override: fishjam -package_name_override: _openapi_client +package_name_override: _fishjam_openapi_client diff --git a/pyproject.toml b/pyproject.toml index 04de671..92c19db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,8 +90,8 @@ ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D107", "DOC502"] convention = "google" [tool.ruff.lint.extend-per-file-ignores] -"fishjam/_openapi_client/**" = ["E501", "D", "DOC"] -"fishjam/_composition_client/**" = ["E501", "D", "DOC"] +"fishjam/_fishjam_openapi_client/**" = ["E501", "D", "DOC"] +"fishjam/_composition_openapi_client/**" = ["E501", "D", "DOC"] "fishjam/events/_protos/**" = ["D", "DOC"] "fishjam/errors.py" = ["D415", "D419", "DOC501", "DOC201"] "**/__init__.py" = ["D415", "DOC"] @@ -111,8 +111,8 @@ exclude = [ ".pytest_cache", ".ruff_cache", "lib", - "fishjam/_openapi_client", - "fishjam/_composition_client", + "fishjam/_fishjam_openapi_client", + "fishjam/_composition_openapi_client", "tests", ] typeCheckingMode = "basic" diff --git a/scripts.py b/scripts.py index a08dd66..0b08627 100644 --- a/scripts.py +++ b/scripts.py @@ -79,7 +79,7 @@ def generate_docs(): def clean_mdx_content(content: str) -> str: parts = re.split(r"((?:```[\s\S]*?```|`[^`\n]+`))", content) - # example: convert `fishjam._openapi_client.models.peer.Peer` into `Peer` + # example: convert `fishjam._fishjam_openapi_client.models.peer.Peer` into `Peer` internal_path_pattern = r"fishjam\.(?:[\w.]+\.)?_[\w.]+\." cleaned_parts = [] @@ -162,7 +162,7 @@ def update_client(): _generate_client( _spec_argument("fishjam openapi.yaml"), "openapi-python-client-config.yaml", - "fishjam/_openapi_client/", + "fishjam/_fishjam_openapi_client/", ) @@ -170,7 +170,7 @@ def update_composition_client(): _generate_client( _spec_argument("Composition API openapi.json"), "openapi-python-client-composition-config.yaml", - "fishjam/_composition_client/", + "fishjam/_composition_openapi_client/", ) diff --git a/tests/test_composition.py b/tests/test_composition.py index 0ff5b05..7462992 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -402,7 +402,7 @@ class TestPublicModels: def test_exports_every_model_except_the_wire_envelopes(self): import fishjam.composition as public - from fishjam._composition_client import models as generated + from fishjam._composition_openapi_client import models as generated assert set(public.__all__) == ( set(generated.__all__) - self.ENVELOPES | self.ALIASES diff --git a/tests/test_room_api.py b/tests/test_room_api.py index a0bcf8c..1287cf8 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -12,7 +12,7 @@ Room, RoomOptions, ) -from fishjam._openapi_client.models import SubscribeMode, Subscriptions +from fishjam._fishjam_openapi_client.models import SubscribeMode, Subscriptions from fishjam.errors import ( BadRequestError, ConflictError, From 49eebe2581d5396af1d7003be42b4a8f2deda609 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Thu, 27 Aug 2026 14:18:09 +0200 Subject: [PATCH 6/7] Report Composition API failures as the server states them A status outside the standard set raised ValueError from the generated client instead of an HTTPError, and messages were wrapped in a list, so they read as ['gone'] where the Fishjam ones read as text. --- fishjam/api/_composition_client.py | 18 +++++++++++------- fishjam/errors.py | 16 ++++++++-------- tests/test_composition.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/fishjam/api/_composition_client.py b/fishjam/api/_composition_client.py index 4ff256e..261d2af 100644 --- a/fishjam/api/_composition_client.py +++ b/fishjam/api/_composition_client.py @@ -1,7 +1,6 @@ """Composition client used to manage compositions, the video compositing sessions.""" from dataclasses import dataclass -from http import HTTPStatus from io import BytesIO from pathlib import Path from typing import Any, TypeVar, cast @@ -144,7 +143,7 @@ class Mp4InputDurations: def _to_error( - status_code: HTTPStatus, error: ApiError | None, not_found: type[HTTPError] + status_code: int, error: ApiError | None, not_found: type[HTTPError] ) -> HTTPError: """Turn a failed Composition API response into the matching Fishjam error. @@ -156,7 +155,7 @@ def _to_error( Returns: The error to raise. """ - return error_for_status(status_code, [error.message] if error else [], not_found) + return error_for_status(status_code, error.message if error else "", not_found) def _to_file(source: FileSource, name: str) -> File: @@ -221,10 +220,15 @@ def _request( response = method.sync_detailed(client=self.client, **kwargs) except UnexpectedStatus as status: raise error_for_status( - HTTPStatus(status.status_code), - [status.content.decode(errors="replace")], + status.status_code, + status.content.decode(errors="replace"), not_found, ) from status + except ValueError as error: + raise InternalServerError( + f"The Composition API answered with a status the client cannot " + f"interpret: {error}" + ) from error if isinstance(response.parsed, ApiError): raise _to_error(response.status_code, response.parsed, not_found) @@ -357,10 +361,10 @@ def register_whip_input( token = _or_none(response.bearer_token) or bearer_token if not token: - raise InternalServerError([ + raise InternalServerError( f'Registering WHIP input "{input_id}" returned no bearer token, ' "so it cannot be published to" - ]) + ) route = _or_none(response.endpoint_route) or f"/whip/{quote(input_id, safe='')}" diff --git a/fishjam/errors.py b/fishjam/errors.py index f752acc..2b564fa 100644 --- a/fishjam/errors.py +++ b/fishjam/errors.py @@ -107,27 +107,27 @@ def __init__(self, errors): def error_for_status( - status_code: HTTPStatus, messages, not_found: type["HTTPError"] | None = None + status_code: int, detail, not_found: type["HTTPError"] | None = None ) -> HTTPError: """@private""" match status_code: case HTTPStatus.BAD_REQUEST | HTTPStatus.UNPROCESSABLE_ENTITY: - return BadRequestError(messages) + return BadRequestError(detail) case HTTPStatus.UNAUTHORIZED: - return UnauthorizedError(messages) + return UnauthorizedError(detail) case HTTPStatus.PAYMENT_REQUIRED: - return QuotaExceededError(messages) + return QuotaExceededError(detail) case HTTPStatus.NOT_FOUND: - return (not_found or NotFoundError)(messages) + return (not_found or NotFoundError)(detail) case HTTPStatus.CONFLICT: - return ConflictError(messages) + return ConflictError(detail) case HTTPStatus.SERVICE_UNAVAILABLE: - return ServiceUnavailableError(messages) + return ServiceUnavailableError(detail) case _: - return InternalServerError(messages) + return InternalServerError(detail) diff --git a/tests/test_composition.py b/tests/test_composition.py index 7462992..c5efc15 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -362,6 +362,18 @@ def test_reports_a_missing_composition_everywhere_else(self): with pytest.raises(CompositionNotFoundError): client().start_composition(COMPOSITION_ID) + def test_maps_a_status_outside_the_standard_set(self): + with mock_response({"message": "gateway"}, status=520): + with pytest.raises(InternalServerError): + client().create_composition() + + def test_carries_the_server_message_as_text(self): + with mock_response({"message": "gone"}, status=404): + with pytest.raises(InputNotFoundError) as raised: + client().unregister_input(COMPOSITION_ID, INPUT_ID) + + assert str(raised.value) == "gone" + def test_maps_a_payment_required_to_a_quota_error(self): with mock_response({"message": "over quota"}, status=402): with pytest.raises(QuotaExceededError): From ec49f4e4627c6bb9fa54b13668bbd577132ae8a0 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Thu, 27 Aug 2026 14:18:09 +0200 Subject: [PATCH 7/7] Start the demo's services with the server Building them at import allocated a room and a composition before startup, and leaked the room when the composition failed. --- examples/composition/composition/app.py | 55 ++++++++++++++++++------- examples/composition/main.py | 28 +------------ 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/examples/composition/composition/app.py b/examples/composition/composition/app.py index 4e4c676..3fc1a35 100644 --- a/examples/composition/composition/app.py +++ b/examples/composition/composition/app.py @@ -1,3 +1,5 @@ +from contextlib import asynccontextmanager + from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware @@ -9,34 +11,54 @@ from .config import COMPOSITION_URL, FISHJAM_ID, FISHJAM_TOKEN from .fishjam_service import FishjamService -fishjam = FishjamService(FISHJAM_ID, FISHJAM_TOKEN) -composition = CompositionService(FISHJAM_TOKEN, COMPOSITION_URL) - - -def start_composing() -> None: - composition.register_assets() - composition.play_movie() - composition.camera() - composition.stream_to( - fishjam.livestream_whip_url(), fishjam.create_streamer_token() - ) +def clean_up(*services) -> None: + for service in services: + if service is None: + continue -def cleanup() -> None: - for service in (composition, fishjam): try: service.cleanup() except Exception as error: print(f"cleanup failed: {error}") -async def streamer(_request: Request) -> Response: - camera = composition.camera() +@asynccontextmanager +async def lifespan(app: Starlette): + fishjam = composition = None + + try: + fishjam = FishjamService(FISHJAM_ID, FISHJAM_TOKEN) + composition = CompositionService(FISHJAM_TOKEN, COMPOSITION_URL) + composition.register_assets() + composition.play_movie() + composition.camera() + composition.stream_to( + fishjam.livestream_whip_url(), fishjam.create_streamer_token() + ) + except Exception: + clean_up(composition, fishjam) + raise + + app.state.fishjam = fishjam + app.state.composition = composition + + try: + yield + finally: + clean_up(composition, fishjam) + print("deleted the composition and the livestream room") + + +async def streamer(request: Request) -> Response: + camera = request.app.state.composition.camera() return JSONResponse({"url": camera.url, "token": camera.bearer_token}) -async def viewer(_request: Request) -> Response: +async def viewer(request: Request) -> Response: + fishjam = request.app.state.fishjam + return JSONResponse({ "url": fishjam.livestream_whep_url(), "token": fishjam.create_viewer_token(), @@ -44,6 +66,7 @@ async def viewer(_request: Request) -> Response: app = Starlette( + lifespan=lifespan, routes=[ Route("/streamer", streamer, methods=["GET"]), Route("/viewer", viewer, methods=["GET"]), diff --git a/examples/composition/main.py b/examples/composition/main.py index 8043138..f49059f 100644 --- a/examples/composition/main.py +++ b/examples/composition/main.py @@ -1,32 +1,8 @@ -import sys -from contextlib import asynccontextmanager - import uvicorn -from composition.app import app, cleanup, start_composing +from composition.app import app from composition.config import HOST, PORT - -@asynccontextmanager -async def lifespan(_app): - try: - start_composing() - except Exception as error: - print(f"failed to start composing: {error}", file=sys.stderr) - cleanup() - raise SystemExit(1) from error - +if __name__ == "__main__": print(f"streamer credentials on http://{HOST}:{PORT}/streamer") print(f"viewer token on http://{HOST}:{PORT}/viewer") - - try: - yield - finally: - cleanup() - print("deleted the composition and the livestream room") - - -app.router.lifespan_context = lifespan - - -if __name__ == "__main__": uvicorn.run(app, host=HOST, port=PORT, log_level="info")