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..3fc1a35
--- /dev/null
+++ b/examples/composition/composition/app.py
@@ -0,0 +1,83 @@
+from contextlib import asynccontextmanager
+
+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
+
+
+def clean_up(*services) -> None:
+ for service in services:
+ if service is None:
+ continue
+
+ try:
+ service.cleanup()
+ except Exception as error:
+ print(f"cleanup failed: {error}")
+
+
+@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:
+ fishjam = request.app.state.fishjam
+
+ return JSONResponse({
+ "url": fishjam.livestream_whep_url(),
+ "token": fishjam.create_viewer_token(),
+ })
+
+
+app = Starlette(
+ lifespan=lifespan,
+ 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..f49059f
--- /dev/null
+++ b/examples/composition/main.py
@@ -0,0 +1,8 @@
+import uvicorn
+from composition.app import app
+from composition.config import HOST, PORT
+
+if __name__ == "__main__":
+ print(f"streamer credentials on http://{HOST}:{PORT}/streamer")
+ print(f"viewer token on http://{HOST}:{PORT}/viewer")
+ 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/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 d3e2017..7703f64 100644
--- a/fishjam/__init__.py
+++ b/fishjam/__init__.py
@@ -8,8 +8,18 @@
# 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._openapi_client.models import PeerMetadata
+from fishjam import (
+ agent,
+ composition,
+ errors,
+ events,
+ integrations,
+ peer,
+ recording,
+ room,
+ version,
+)
+from fishjam._fishjam_openapi_client.models import PeerMetadata
# API
from fishjam._webhook_notifier import (
@@ -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/_composition_openapi_client/__init__.py b/fishjam/_composition_openapi_client/__init__.py
new file mode 100644
index 0000000..44f789d
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/api/__init__.py b/fishjam/_composition_openapi_client/api/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/__init__.py
rename to fishjam/_composition_openapi_client/api/__init__.py
diff --git a/fishjam/_openapi_client/api/credentials/__init__.py b/fishjam/_composition_openapi_client/api/compositions/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/credentials/__init__.py
rename to fishjam/_composition_openapi_client/api/compositions/__init__.py
diff --git a/fishjam/_composition_openapi_client/api/compositions/create_composition.py b/fishjam/_composition_openapi_client/api/compositions/create_composition.py
new file mode 100644
index 0000000..10ba341
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/compositions/delete_composition.py b/fishjam/_composition_openapi_client/api/compositions/delete_composition.py
new file mode 100644
index 0000000..611a652
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/compositions/reset.py b/fishjam/_composition_openapi_client/api/compositions/reset.py
new file mode 100644
index 0000000..57d81de
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/compositions/start.py b/fishjam/_composition_openapi_client/api/compositions/start.py
new file mode 100644
index 0000000..901ce84
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/api/mo_q/__init__.py b/fishjam/_composition_openapi_client/api/events/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/mo_q/__init__.py
rename to fishjam/_composition_openapi_client/api/events/__init__.py
diff --git a/fishjam/_composition_openapi_client/api/events/send_composition_event.py b/fishjam/_composition_openapi_client/api/events/send_composition_event.py
new file mode 100644
index 0000000..823f285
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/api/recordings/__init__.py b/fishjam/_composition_openapi_client/api/inputs/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/recordings/__init__.py
rename to fishjam/_composition_openapi_client/api/inputs/__init__.py
diff --git a/fishjam/_composition_openapi_client/api/inputs/register_input.py b/fishjam/_composition_openapi_client/api/inputs/register_input.py
new file mode 100644
index 0000000..263486d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/inputs/unregister_input.py b/fishjam/_composition_openapi_client/api/inputs/unregister_input.py
new file mode 100644
index 0000000..99583a8
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/api/rooms/__init__.py b/fishjam/_composition_openapi_client/api/media_transport/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/rooms/__init__.py
rename to fishjam/_composition_openapi_client/api/media_transport/__init__.py
diff --git a/fishjam/_openapi_client/api/streamers/__init__.py b/fishjam/_composition_openapi_client/api/outputs/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/streamers/__init__.py
rename to fishjam/_composition_openapi_client/api/outputs/__init__.py
diff --git a/fishjam/_composition_openapi_client/api/outputs/register_output.py b/fishjam/_composition_openapi_client/api/outputs/register_output.py
new file mode 100644
index 0000000..58bb1fa
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/outputs/register_template_output.py b/fishjam/_composition_openapi_client/api/outputs/register_template_output.py
new file mode 100644
index 0000000..0ed92f1
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/outputs/request_keyframe.py b/fishjam/_composition_openapi_client/api/outputs/request_keyframe.py
new file mode 100644
index 0000000..62ce71b
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/outputs/unregister_output.py b/fishjam/_composition_openapi_client/api/outputs/unregister_output.py
new file mode 100644
index 0000000..bc55b01
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/outputs/update_output.py b/fishjam/_composition_openapi_client/api/outputs/update_output.py
new file mode 100644
index 0000000..df65d06
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/api/streams/__init__.py b/fishjam/_composition_openapi_client/api/renderers/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/streams/__init__.py
rename to fishjam/_composition_openapi_client/api/renderers/__init__.py
diff --git a/fishjam/_composition_openapi_client/api/renderers/register_font.py b/fishjam/_composition_openapi_client/api/renderers/register_font.py
new file mode 100644
index 0000000..235992d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/renderers/register_image.py b/fishjam/_composition_openapi_client/api/renderers/register_image.py
new file mode 100644
index 0000000..0924e26
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/api/renderers/unregister_image.py b/fishjam/_composition_openapi_client/api/renderers/unregister_image.py
new file mode 100644
index 0000000..f367eff
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/client.py b/fishjam/_composition_openapi_client/client.py
similarity index 100%
rename from fishjam/_openapi_client/client.py
rename to fishjam/_composition_openapi_client/client.py
diff --git a/fishjam/_openapi_client/errors.py b/fishjam/_composition_openapi_client/errors.py
similarity index 100%
rename from fishjam/_openapi_client/errors.py
rename to fishjam/_composition_openapi_client/errors.py
diff --git a/fishjam/_composition_openapi_client/models/__init__.py b/fishjam/_composition_openapi_client/models/__init__.py
new file mode 100644
index 0000000..1a5e00a
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/api_error.py b/fishjam/_composition_openapi_client/models/api_error.py
new file mode 100644
index 0000000..7286535
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/audio_channels.py b/fishjam/_composition_openapi_client/models/audio_channels.py
new file mode 100644
index 0000000..6b74e5d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/audio_mixing_strategy.py b/fishjam/_composition_openapi_client/models/audio_mixing_strategy.py
new file mode 100644
index 0000000..49c1e9a
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/audio_scene.py b/fishjam/_composition_openapi_client/models/audio_scene.py
new file mode 100644
index 0000000..b55c23c
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/audio_scene_input.py b/fishjam/_composition_openapi_client/models/audio_scene_input.py
new file mode 100644
index 0000000..1259037
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/average_and_max_bitrate.py b/fishjam/_composition_openapi_client/models/average_and_max_bitrate.py
new file mode 100644
index 0000000..64999a2
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/box_shadow.py b/fishjam/_composition_openapi_client/models/box_shadow.py
new file mode 100644
index 0000000..f6c682c
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/composition_created_response.py b/fishjam/_composition_openapi_client/models/composition_created_response.py
new file mode 100644
index 0000000..a6dc274
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/create_composition_request.py b/fishjam/_composition_openapi_client/models/create_composition_request.py
new file mode 100644
index 0000000..d737a04
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/easing_function_bounce.py b/fishjam/_composition_openapi_client/models/easing_function_bounce.py
new file mode 100644
index 0000000..b5125f6
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/easing_function_bounce_function_name.py b/fishjam/_composition_openapi_client/models/easing_function_bounce_function_name.py
new file mode 100644
index 0000000..ccbc9f1
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/easing_function_cubic_bezier.py b/fishjam/_composition_openapi_client/models/easing_function_cubic_bezier.py
new file mode 100644
index 0000000..7a96ee8
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/easing_function_cubic_bezier_function_name.py b/fishjam/_composition_openapi_client/models/easing_function_cubic_bezier_function_name.py
new file mode 100644
index 0000000..4b63e27
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/easing_function_linear.py b/fishjam/_composition_openapi_client/models/easing_function_linear.py
new file mode 100644
index 0000000..1112a4a
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/easing_function_linear_function_name.py b/fishjam/_composition_openapi_client/models/easing_function_linear_function_name.py
new file mode 100644
index 0000000..7f64cb3
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/empty_response.py b/fishjam/_composition_openapi_client/models/empty_response.py
new file mode 100644
index 0000000..1d1f80c
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/font_upload.py b/fishjam/_composition_openapi_client/models/font_upload.py
new file mode 100644
index 0000000..cc7d6f3
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/h264_encoder_preset.py b/fishjam/_composition_openapi_client/models/h264_encoder_preset.py
new file mode 100644
index 0000000..2ec1e58
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/horizontal_align.py b/fishjam/_composition_openapi_client/models/horizontal_align.py
new file mode 100644
index 0000000..4849499
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image.py b/fishjam/_composition_openapi_client/models/image.py
new file mode 100644
index 0000000..01804d9
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_auto.py b/fishjam/_composition_openapi_client/models/image_spec_auto.py
new file mode 100644
index 0000000..bc91d3c
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_auto_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_auto_asset_type.py
new file mode 100644
index 0000000..f4b4677
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_gif.py b/fishjam/_composition_openapi_client/models/image_spec_gif.py
new file mode 100644
index 0000000..36ee2d7
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_gif_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_gif_asset_type.py
new file mode 100644
index 0000000..940c3bc
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_jpeg.py b/fishjam/_composition_openapi_client/models/image_spec_jpeg.py
new file mode 100644
index 0000000..ebbffab
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_jpeg_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_jpeg_asset_type.py
new file mode 100644
index 0000000..560b00a
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_png.py b/fishjam/_composition_openapi_client/models/image_spec_png.py
new file mode 100644
index 0000000..204a207
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_png_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_png_asset_type.py
new file mode 100644
index 0000000..965a602
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_svg.py b/fishjam/_composition_openapi_client/models/image_spec_svg.py
new file mode 100644
index 0000000..4f541e2
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_spec_svg_asset_type.py b/fishjam/_composition_openapi_client/models/image_spec_svg_asset_type.py
new file mode 100644
index 0000000..bdb462d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/image_type.py b/fishjam/_composition_openapi_client/models/image_type.py
new file mode 100644
index 0000000..bf4d3a2
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/input_stream.py b/fishjam/_composition_openapi_client/models/input_stream.py
new file mode 100644
index 0000000..1d214ca
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/input_stream_type.py b/fishjam/_composition_openapi_client/models/input_stream_type.py
new file mode 100644
index 0000000..ebfbcf6
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/interpolation.py b/fishjam/_composition_openapi_client/models/interpolation.py
new file mode 100644
index 0000000..813f22e
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/mp_4_input.py b/fishjam/_composition_openapi_client/models/mp_4_input.py
new file mode 100644
index 0000000..ec620a4
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/mp_4_input_type.py b/fishjam/_composition_openapi_client/models/mp_4_input_type.py
new file mode 100644
index 0000000..6482917
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/opus_encoder_preset.py b/fishjam/_composition_openapi_client/models/opus_encoder_preset.py
new file mode 100644
index 0000000..e646fa1
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/output_end_condition.py b/fishjam/_composition_openapi_client/models/output_end_condition.py
new file mode 100644
index 0000000..ef83826
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/output_rtmp_client_audio_options.py b/fishjam/_composition_openapi_client/models/output_rtmp_client_audio_options.py
new file mode 100644
index 0000000..3aed6aa
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/output_rtmp_client_video_options.py b/fishjam/_composition_openapi_client/models/output_rtmp_client_video_options.py
new file mode 100644
index 0000000..0c52a14
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/output_whip_audio_options.py b/fishjam/_composition_openapi_client/models/output_whip_audio_options.py
new file mode 100644
index 0000000..4fa0584
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/output_whip_video_options.py b/fishjam/_composition_openapi_client/models/output_whip_video_options.py
new file mode 100644
index 0000000..d47638b
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/overflow.py b/fishjam/_composition_openapi_client/models/overflow.py
new file mode 100644
index 0000000..2b38466
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/pixel_format.py b/fishjam/_composition_openapi_client/models/pixel_format.py
new file mode 100644
index 0000000..d22422d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/register_font_body.py b/fishjam/_composition_openapi_client/models/register_font_body.py
new file mode 100644
index 0000000..96d1ff5
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/register_input_response.py b/fishjam/_composition_openapi_client/models/register_input_response.py
new file mode 100644
index 0000000..da66255
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/register_template_output.py b/fishjam/_composition_openapi_client/models/register_template_output.py
new file mode 100644
index 0000000..bb2a25b
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/register_template_output_body.py b/fishjam/_composition_openapi_client/models/register_template_output_body.py
new file mode 100644
index 0000000..75d6c95
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rescale_mode.py b/fishjam/_composition_openapi_client/models/rescale_mode.py
new file mode 100644
index 0000000..465ca1b
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rescaler.py b/fishjam/_composition_openapi_client/models/rescaler.py
new file mode 100644
index 0000000..b320a96
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rescaler_type.py b/fishjam/_composition_openapi_client/models/rescaler_type.py
new file mode 100644
index 0000000..8d565a8
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/resolution.py b/fishjam/_composition_openapi_client/models/resolution.py
new file mode 100644
index 0000000..2587893
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rtmp_input.py b/fishjam/_composition_openapi_client/models/rtmp_input.py
new file mode 100644
index 0000000..73e5a54
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rtmp_input_type.py b/fishjam/_composition_openapi_client/models/rtmp_input_type.py
new file mode 100644
index 0000000..1b652fa
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rtmp_output.py b/fishjam/_composition_openapi_client/models/rtmp_output.py
new file mode 100644
index 0000000..034b313
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/rtmp_output_type.py b/fishjam/_composition_openapi_client/models/rtmp_output_type.py
new file mode 100644
index 0000000..9b5e26d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/send_composition_event_body.py b/fishjam/_composition_openapi_client/models/send_composition_event_body.py
new file mode 100644
index 0000000..d4b8193
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/text.py b/fishjam/_composition_openapi_client/models/text.py
new file mode 100644
index 0000000..2cf50c7
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/text_style.py b/fishjam/_composition_openapi_client/models/text_style.py
new file mode 100644
index 0000000..c601cd9
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/text_type.py b/fishjam/_composition_openapi_client/models/text_type.py
new file mode 100644
index 0000000..9f9de15
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/text_weight.py b/fishjam/_composition_openapi_client/models/text_weight.py
new file mode 100644
index 0000000..4d1cc01
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/text_wrap_mode.py b/fishjam/_composition_openapi_client/models/text_wrap_mode.py
new file mode 100644
index 0000000..febaf4f
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/tiles.py b/fishjam/_composition_openapi_client/models/tiles.py
new file mode 100644
index 0000000..bc22d02
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/tiles_type.py b/fishjam/_composition_openapi_client/models/tiles_type.py
new file mode 100644
index 0000000..e651122
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/transition.py b/fishjam/_composition_openapi_client/models/transition.py
new file mode 100644
index 0000000..20b1265
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/transport_protocol.py b/fishjam/_composition_openapi_client/models/transport_protocol.py
new file mode 100644
index 0000000..4007b0f
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/unregister_input.py b/fishjam/_composition_openapi_client/models/unregister_input.py
new file mode 100644
index 0000000..9854fd1
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/unregister_output.py b/fishjam/_composition_openapi_client/models/unregister_output.py
new file mode 100644
index 0000000..dcd7f7a
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/unregister_renderer.py b/fishjam/_composition_openapi_client/models/unregister_renderer.py
new file mode 100644
index 0000000..b32b9d3
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/update_output_request.py b/fishjam/_composition_openapi_client/models/update_output_request.py
new file mode 100644
index 0000000..5d65a22
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/vertical_align.py b/fishjam/_composition_openapi_client/models/vertical_align.py
new file mode 100644
index 0000000..70054d6
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/video_scene.py b/fishjam/_composition_openapi_client/models/video_scene.py
new file mode 100644
index 0000000..3c5215c
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/view.py b/fishjam/_composition_openapi_client/models/view.py
new file mode 100644
index 0000000..b6e791d
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/view_direction.py b/fishjam/_composition_openapi_client/models/view_direction.py
new file mode 100644
index 0000000..4357992
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/view_type.py b/fishjam/_composition_openapi_client/models/view_type.py
new file mode 100644
index 0000000..29b52ab
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whep_input.py b/fishjam/_composition_openapi_client/models/whep_input.py
new file mode 100644
index 0000000..d5d3934
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whep_input_type.py b/fishjam/_composition_openapi_client/models/whep_input_type.py
new file mode 100644
index 0000000..68520dd
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_audio_encoder_options_any.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_any.py
new file mode 100644
index 0000000..9fc2b3a
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_audio_encoder_options_any_type.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_any_type.py
new file mode 100644
index 0000000..c9c4057
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_audio_encoder_options_opus.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_opus.py
new file mode 100644
index 0000000..dcbad3e
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_audio_encoder_options_opus_type.py b/fishjam/_composition_openapi_client/models/whip_audio_encoder_options_opus_type.py
new file mode 100644
index 0000000..3229a23
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_input.py b/fishjam/_composition_openapi_client/models/whip_input.py
new file mode 100644
index 0000000..11e76e2
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_input_type.py b/fishjam/_composition_openapi_client/models/whip_input_type.py
new file mode 100644
index 0000000..e812626
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_output.py b/fishjam/_composition_openapi_client/models/whip_output.py
new file mode 100644
index 0000000..cabcbe0
--- /dev/null
+++ b/fishjam/_composition_openapi_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_openapi_client/models/whip_output_type.py b/fishjam/_composition_openapi_client/models/whip_output_type.py
new file mode 100644
index 0000000..e2278f6
--- /dev/null
+++ b/fishjam/_composition_openapi_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/_openapi_client/types.py b/fishjam/_composition_openapi_client/types.py
similarity index 100%
rename from fishjam/_openapi_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/_fishjam_openapi_client/api/__init__.py b/fishjam/_fishjam_openapi_client/api/__init__.py
new file mode 100644
index 0000000..81f9fa2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/__init__.py
@@ -0,0 +1 @@
+"""Contains methods for accessing the API"""
diff --git a/fishjam/_openapi_client/api/track_forwardings/__init__.py b/fishjam/_fishjam_openapi_client/api/credentials/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/track_forwardings/__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/viewers/__init__.py b/fishjam/_fishjam_openapi_client/api/mo_q/__init__.py
similarity index 100%
rename from fishjam/_openapi_client/api/viewers/__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/_fishjam_openapi_client/api/recordings/__init__.py b/fishjam/_fishjam_openapi_client/api/recordings/__init__.py
new file mode 100644
index 0000000..2d7c0b2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/recordings/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
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/_fishjam_openapi_client/api/rooms/__init__.py b/fishjam/_fishjam_openapi_client/api/rooms/__init__.py
new file mode 100644
index 0000000..2d7c0b2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/rooms/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
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/_fishjam_openapi_client/api/streamers/__init__.py b/fishjam/_fishjam_openapi_client/api/streamers/__init__.py
new file mode 100644
index 0000000..2d7c0b2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/streamers/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
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/_fishjam_openapi_client/api/streams/__init__.py b/fishjam/_fishjam_openapi_client/api/streams/__init__.py
new file mode 100644
index 0000000..2d7c0b2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/streams/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
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/_fishjam_openapi_client/api/track_forwardings/__init__.py b/fishjam/_fishjam_openapi_client/api/track_forwardings/__init__.py
new file mode 100644
index 0000000..2d7c0b2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/track_forwardings/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
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/_fishjam_openapi_client/api/viewers/__init__.py b/fishjam/_fishjam_openapi_client/api/viewers/__init__.py
new file mode 100644
index 0000000..2d7c0b2
--- /dev/null
+++ b/fishjam/_fishjam_openapi_client/api/viewers/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
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/_fishjam_openapi_client/client.py b/fishjam/_fishjam_openapi_client/client.py
new file mode 100644
index 0000000..0ab1589
--- /dev/null
+++ b/fishjam/_fishjam_openapi_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/_fishjam_openapi_client/errors.py b/fishjam/_fishjam_openapi_client/errors.py
new file mode 100644
index 0000000..5f92e76
--- /dev/null
+++ b/fishjam/_fishjam_openapi_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/_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/_fishjam_openapi_client/types.py b/fishjam/_fishjam_openapi_client/types.py
new file mode 100644
index 0000000..b64af09
--- /dev/null
+++ b/fishjam/_fishjam_openapi_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/fishjam/api/_client.py b/fishjam/api/_client.py
index 10541ac..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
@@ -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/_composition_client.py b/fishjam/api/_composition_client.py
new file mode 100644
index 0000000..261d2af
--- /dev/null
+++ b/fishjam/api/_composition_client.py
@@ -0,0 +1,725 @@
+"""Composition client used to manage compositions, the video compositing sessions."""
+
+from dataclasses import dataclass
+from io import BytesIO
+from pathlib import Path
+from typing import Any, TypeVar, cast
+from urllib.parse import quote
+
+from fishjam._composition_openapi_client.api.compositions import (
+ create_composition as compositions_create,
+)
+from fishjam._composition_openapi_client.api.compositions import (
+ delete_composition as compositions_delete,
+)
+from fishjam._composition_openapi_client.api.compositions import (
+ reset as compositions_reset,
+)
+from fishjam._composition_openapi_client.api.compositions import (
+ start as compositions_start,
+)
+from fishjam._composition_openapi_client.api.events import (
+ send_composition_event as events_send,
+)
+from fishjam._composition_openapi_client.api.inputs import (
+ register_input as inputs_register,
+)
+from fishjam._composition_openapi_client.api.inputs import (
+ unregister_input as inputs_unregister,
+)
+from fishjam._composition_openapi_client.api.outputs import (
+ register_output as outputs_register,
+)
+from fishjam._composition_openapi_client.api.outputs import (
+ register_template_output as outputs_register_template,
+)
+from fishjam._composition_openapi_client.api.outputs import (
+ request_keyframe as outputs_request_keyframe,
+)
+from fishjam._composition_openapi_client.api.outputs import (
+ unregister_output as outputs_unregister,
+)
+from fishjam._composition_openapi_client.api.outputs import (
+ update_output as outputs_update,
+)
+from fishjam._composition_openapi_client.api.renderers import (
+ register_font as renderers_register_font,
+)
+from fishjam._composition_openapi_client.api.renderers import (
+ register_image as renderers_register_image,
+)
+from fishjam._composition_openapi_client.api.renderers import (
+ unregister_image as renderers_unregister_image,
+)
+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,
+ 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_openapi_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: int, 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(
+ 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)
+
+ 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/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py
index b7c352e..44ce2db 100644
--- a/fishjam/api/_fishjam_client.py
+++ b/fishjam/api/_fishjam_client.py
@@ -4,46 +4,58 @@
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.viewers import (
+from fishjam._fishjam_openapi_client.api.track_forwardings import (
+ create_track_forwarding as track_forwardings_create,
+)
+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,
+ CompositionInfo,
CompositionSource,
ListRecordingsMetadata,
MoqAccess,
@@ -73,16 +85,18 @@
StreamerToken,
SubscribeMode,
SubscribeTracksBody,
+ TrackForwarding,
VideoCodec,
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 (
InvalidFishjamCredentialsError,
)
+from fishjam.utils import get_livestream_whep_url, get_livestream_whip_url
@dataclass
@@ -93,6 +107,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 +117,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 +377,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 +387,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 +402,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 +438,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 +701,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/composition/__init__.py b/fishjam/composition/__init__.py
new file mode 100644
index 0000000..48588f9
--- /dev/null
+++ b/fishjam/composition/__init__.py
@@ -0,0 +1,183 @@
+from pathlib import Path
+
+from fishjam._composition_openapi_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..2b564fa 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):
@@ -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: int, detail, not_found: type["HTTPError"] | None = None
+) -> HTTPError:
+ """@private"""
+ match status_code:
+ case HTTPStatus.BAD_REQUEST | HTTPStatus.UNPROCESSABLE_ENTITY:
+ return BadRequestError(detail)
+
+ case HTTPStatus.UNAUTHORIZED:
+ return UnauthorizedError(detail)
+
+ case HTTPStatus.PAYMENT_REQUIRED:
+ return QuotaExceededError(detail)
+
+ case HTTPStatus.NOT_FOUND:
+ return (not_found or NotFoundError)(detail)
+
+ case HTTPStatus.CONFLICT:
+ return ConflictError(detail)
+
+ case HTTPStatus.SERVICE_UNAVAILABLE:
+ return ServiceUnavailableError(detail)
+
+ case _:
+ return InternalServerError(detail)
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/fishjam/utils.py b/fishjam/utils.py
index 2434966..9a203a6 100644
--- a/fishjam/utils.py
+++ b/fishjam/utils.py
@@ -2,6 +2,10 @@
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:
try:
@@ -19,3 +23,48 @@ 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}"
+
+
+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/openapi-python-client-composition-config.yaml b/openapi-python-client-composition-config.yaml
new file mode 100644
index 0000000..57c33ab
--- /dev/null
+++ b/openapi-python-client-composition-config.yaml
@@ -0,0 +1,2 @@
+project_name_override: fishjam
+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 f8bffbf..92c19db 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]
@@ -60,6 +61,7 @@ default-groups = ["dev", "test"]
[tool.uv.workspace]
members = [
+ "examples/composition",
"examples/transcription",
".",
"examples/poet_chat",
@@ -88,7 +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/_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"]
@@ -108,7 +111,8 @@ exclude = [
".pytest_cache",
".ruff_cache",
"lib",
- "fishjam/_openapi_client",
+ "fishjam/_fishjam_openapi_client",
+ "fishjam/_composition_openapi_client",
"tests",
]
typeCheckingMode = "basic"
diff --git a/scripts.py b/scripts.py
index 8a23a98..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 = []
@@ -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/_fishjam_openapi_client/",
+ )
+
+
+def update_composition_client():
+ _generate_client(
+ _spec_argument("Composition API openapi.json"),
+ "openapi-python-client-composition-config.yaml",
+ "fishjam/_composition_openapi_client/",
+ )
+
+
def start_room_manager():
current_path = os.getcwd()
current_folder = os.path.basename(current_path)
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..c5efc15
--- /dev/null
+++ b/tests/test_composition.py
@@ -0,0 +1,431 @@
+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_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):
+ 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_openapi_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))
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,
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
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"