From cf9c640409ce0f6789408119163ea58c04dafb5d Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Thu, 15 Jan 2026 16:29:36 +0100 Subject: [PATCH 01/18] sent sdk version in headers --- fishjam/_openapi_client/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index eeffd00..eb4e23c 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -4,6 +4,8 @@ import httpx from attrs import define, evolve, field +from fishjam.version import get_version + @define class Client: @@ -229,6 +231,7 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) + self._headers["x-sdk_version"] = f"py-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -265,6 +268,7 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) + self._headers["x-sdk_version"] = f"py-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, From 80f2b287e3a0e879c5d4ecd8a9aaf3918eeba5b9 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 19 Jan 2026 15:43:14 +0100 Subject: [PATCH 02/18] comment suggestions, tests --- fishjam/_openapi_client/client.py | 7 +++-- tests/test_openapi_client_headers.py | 41 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 tests/test_openapi_client_headers.py diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index eb4e23c..0bf2dd8 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -193,6 +193,9 @@ class AuthenticatedClient: prefix: str = "Bearer" auth_header_name: str = "Authorization" + api_prefix: str = "python-server" + api_header_name: str = "x-fishjam-api-client" + 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: @@ -231,7 +234,7 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-sdk_version"] = f"py-{get_version()}" + self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -268,7 +271,7 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-sdk_version"] = f"py-{get_version()}" + self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, diff --git a/tests/test_openapi_client_headers.py b/tests/test_openapi_client_headers.py new file mode 100644 index 0000000..e58617b --- /dev/null +++ b/tests/test_openapi_client_headers.py @@ -0,0 +1,41 @@ +import pytest + +from fishjam._openapi_client.client import AuthenticatedClient +from fishjam.version import get_version + + +def test_authenticated_client_sets_sdk_and_auth_headers_sync(): + client = AuthenticatedClient( + base_url="https://example.com", + token="token123", + headers={"custom": "value"}, + ) + + httpx_client = client.get_httpx_client() + try: + headers = httpx_client.headers + + assert headers[client.auth_header_name] == "Bearer token123" + assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" + assert headers["custom"] == "value" + finally: + httpx_client.close() + + +@pytest.mark.asyncio +async def test_authenticated_client_sets_sdk_and_auth_headers_async(): + client = AuthenticatedClient( + base_url="https://example.com", + token="token456", + headers={"another": "header"}, + ) + + async_client = client.get_async_httpx_client() + try: + headers = async_client.headers + + assert headers[client.auth_header_name] == "Bearer token456" + assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" + assert headers["another"] == "header" + finally: + await async_client.aclose() From a28ab40fbbfaf11f248dc6efe2ea9b89f105edf9 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 19 Jan 2026 19:15:48 +0100 Subject: [PATCH 03/18] add client headers to template since _clinet.py is autogenerated --- templates/openapi/client.py.jinja | 193 ++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 templates/openapi/client.py.jinja diff --git a/templates/openapi/client.py.jinja b/templates/openapi/client.py.jinja new file mode 100644 index 0000000..a99fc2e --- /dev/null +++ b/templates/openapi/client.py.jinja @@ -0,0 +1,193 @@ +import ssl +from typing import Any, Union, Optional + +from attrs import define, field, evolve +import httpx + +from fishjam.version import get_version + + +{% set attrs_info = { + "raise_on_unexpected_status": namespace( + type="bool", + default="field(default=False, kw_only=True)", + docstring="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": namespace(type="str", default="", docstring="The token to use for authentication"), + "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), + "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), +} %} + +{% macro attr_in_class_docstring(name) %} +{{ name }}: {{ attrs_info[name].docstring }} +{%- endmacro %} + +{% macro declare_attr(name) %} +{% set attr = attrs_info[name] %} +{{ name }}: {{ attr.type }}{% if attr.default %} = {{ attr.default }}{% endif %} +{% if attr.docstring and config.docstrings_on_attributes +%} +"""{{ attr.docstring }}""" +{%- endif %} +{% endmacro %} + +@define +class Client: + """A class for keeping track of data related to the API + +{% macro httpx_args_docstring() %} + 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. +{% endmacro %} +{{ httpx_args_docstring() }} +{% if not config.docstrings_on_attributes %} + + Attributes: + {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} +{% endif %} + """ +{% macro attributes() %} + {{ declare_attr("raise_on_unexpected_status") | indent(4) }} + _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: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: Union[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: Optional[httpx.Client] = field(default=None, init=False) + _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) +{% endmacro %}{{ attributes() }} +{% macro builders(self) %} + def with_headers(self, headers: dict[str, str]) -> "{{ self }}": + """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]) -> "{{ self }}": + """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) -> "{{ self }}": + """Get a new client matching this one with a new timeout (in seconds)""" + 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) +{% endmacro %}{{ builders("Client") }} +{% macro httpx_stuff(name, custom_constructor=None) %} + def set_httpx_client(self, client: httpx.Client) -> "{{ name }}": + """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: + {% if custom_constructor %} + {{ custom_constructor | indent(12) }} + {% endif %} + 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) -> "{{ name }}": + """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) -> "{{ name }}": + """Manually 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: + {% if custom_constructor %} + {{ custom_constructor | indent(12) }} + {% endif %} + 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) -> "{{ name }}": + """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) +{% endmacro %}{{ httpx_stuff("Client") }} + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + +{{ httpx_args_docstring() }} +{% if not config.docstrings_on_attributes %} + + Attributes: + {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} + {{ attr_in_class_docstring("token") | indent(8) }} + {{ attr_in_class_docstring("prefix") | indent(8) }} + {{ attr_in_class_docstring("auth_header_name") | indent(8) }} +{% endif %} + """ + +{{ attributes() }} + {{ declare_attr("token") | indent(4) }} + {{ declare_attr("prefix") | indent(4) }} + {{ declare_attr("auth_header_name") | indent(4) }} + +{{ builders("AuthenticatedClient") }} +{{ httpx_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token; self._headers[\"x-fishjam-api-client\"] = f\"python-server-{get_version()}\"") }} From 29eee7d4997c402b4a5e7bf5006b2ca984d56842 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Mon, 19 Jan 2026 19:16:25 +0100 Subject: [PATCH 04/18] add autogenerated client --- fishjam/_openapi_client/client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index 0bf2dd8..64fd976 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -193,9 +193,6 @@ class AuthenticatedClient: prefix: str = "Bearer" auth_header_name: str = "Authorization" - api_prefix: str = "python-server" - api_header_name: str = "x-fishjam-api-client" - 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: @@ -234,7 +231,7 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" + self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -271,7 +268,7 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" + self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, From 7731e5663e5fdbbc75fa9c777d52b0ada6f5f7a0 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 12:19:33 +0100 Subject: [PATCH 05/18] update jinja and client, move tests to test_room_api.py --- fishjam/_openapi_client/client.py | 10 +++++-- templates/openapi/client.py.jinja | 12 +++++++- tests/test_openapi_client_headers.py | 41 ---------------------------- tests/test_room_api.py | 34 +++++++++++++++++++++++ 4 files changed, 53 insertions(+), 44 deletions(-) delete mode 100644 tests/test_openapi_client_headers.py diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index 64fd976..4b2aac4 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -170,6 +170,8 @@ class AuthenticatedClient: 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 + api_prefix: The prefix to use for the api version header + api_header_name: The name of the api version header """ raise_on_unexpected_status: bool = field(default=False, kw_only=True) @@ -192,6 +194,8 @@ class AuthenticatedClient: token: str prefix: str = "Bearer" auth_header_name: str = "Authorization" + api_prefix: str = "python-server" + api_header_name: str = "x-fishjam-api-client" def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": """Get a new client matching this one with additional headers""" @@ -231,7 +235,8 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" + self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" + self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -268,7 +273,8 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" + self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" + self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, diff --git a/templates/openapi/client.py.jinja b/templates/openapi/client.py.jinja index a99fc2e..88cdd6a 100644 --- a/templates/openapi/client.py.jinja +++ b/templates/openapi/client.py.jinja @@ -18,6 +18,8 @@ from fishjam.version import get_version "token": namespace(type="str", default="", docstring="The token to use for authentication"), "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), + "api_prefix": namespace(type="str", default='"python-server"', docstring="The prefix to use for the api version header"), + "api_header_name": namespace(type="str", default='"x-fishjam-api-client"', docstring="The name of the api version header"), } %} {% macro attr_in_class_docstring(name) %} @@ -181,6 +183,8 @@ class AuthenticatedClient: {{ attr_in_class_docstring("token") | indent(8) }} {{ attr_in_class_docstring("prefix") | indent(8) }} {{ attr_in_class_docstring("auth_header_name") | indent(8) }} + {{ attr_in_class_docstring("api_prefix") | indent(8) }} + {{ attr_in_class_docstring("api_header_name") | indent(8) }} {% endif %} """ @@ -188,6 +192,12 @@ class AuthenticatedClient: {{ declare_attr("token") | indent(4) }} {{ declare_attr("prefix") | indent(4) }} {{ declare_attr("auth_header_name") | indent(4) }} + {{ declare_attr("api_prefix") | indent(4) }} + {{ declare_attr("api_header_name") | indent(4) }} {{ builders("AuthenticatedClient") }} -{{ httpx_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token; self._headers[\"x-fishjam-api-client\"] = f\"python-server-{get_version()}\"") }} +{% set auth_constructor %} +self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token +self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" +{% endset %} +{{ httpx_stuff("AuthenticatedClient", auth_constructor) }} diff --git a/tests/test_openapi_client_headers.py b/tests/test_openapi_client_headers.py deleted file mode 100644 index e58617b..0000000 --- a/tests/test_openapi_client_headers.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest - -from fishjam._openapi_client.client import AuthenticatedClient -from fishjam.version import get_version - - -def test_authenticated_client_sets_sdk_and_auth_headers_sync(): - client = AuthenticatedClient( - base_url="https://example.com", - token="token123", - headers={"custom": "value"}, - ) - - httpx_client = client.get_httpx_client() - try: - headers = httpx_client.headers - - assert headers[client.auth_header_name] == "Bearer token123" - assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" - assert headers["custom"] == "value" - finally: - httpx_client.close() - - -@pytest.mark.asyncio -async def test_authenticated_client_sets_sdk_and_auth_headers_async(): - client = AuthenticatedClient( - base_url="https://example.com", - token="token456", - headers={"another": "header"}, - ) - - async_client = client.get_async_httpx_client() - try: - headers = async_client.headers - - assert headers[client.auth_header_name] == "Bearer token456" - assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" - assert headers["another"] == "header" - finally: - await async_client.aclose() diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 3130bbb..c3aec8f 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -53,6 +53,40 @@ def test_valid_token(self): assert room in all_rooms +class TestApiVersionHeaders: + def test_client_sets_sdk_header_sync(self): + client = FishjamClient( + base_url="https://example.com", + management_token="token123", + ) + httpx_client = client.get_httpx_client() + try: + headers = httpx_client.headers + + assert ( + headers[client.api_header_name] + == f"{client.api_prefix}-{client.get_sdk_version()}" + ) + finally: + httpx_client.close() + + def test_client_sets_sdk_header_async(self): + client = FishjamClient( + base_url="https://example.com", + management_token="token456", + ) + async_client = client.get_async_httpx_client() + try: + headers = async_client.headers + + assert ( + headers[client.api_header_name] + == f"{client.api_prefix}-{client.get_sdk_version()}" + ) + finally: + import asyncio + asyncio.run(async_client.aclose()) + @pytest.fixture def room_api(): From 72c5e82a33485e41752809e8be8af60a29db2131 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 12:27:35 +0100 Subject: [PATCH 06/18] format --- tests/test_room_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index c3aec8f..cdf540a 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -53,6 +53,7 @@ def test_valid_token(self): assert room in all_rooms + class TestApiVersionHeaders: def test_client_sets_sdk_header_sync(self): client = FishjamClient( @@ -69,7 +70,7 @@ def test_client_sets_sdk_header_sync(self): ) finally: httpx_client.close() - + def test_client_sets_sdk_header_async(self): client = FishjamClient( base_url="https://example.com", @@ -85,6 +86,7 @@ def test_client_sets_sdk_header_async(self): ) finally: import asyncio + asyncio.run(async_client.aclose()) From 645f6caba6472374a4071135d374806f96172758 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 12:30:41 +0100 Subject: [PATCH 07/18] fix tests --- tests/test_room_api.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index cdf540a..83ed50a 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -56,10 +56,7 @@ def test_valid_token(self): class TestApiVersionHeaders: def test_client_sets_sdk_header_sync(self): - client = FishjamClient( - base_url="https://example.com", - management_token="token123", - ) + client = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) httpx_client = client.get_httpx_client() try: headers = httpx_client.headers @@ -72,10 +69,7 @@ def test_client_sets_sdk_header_sync(self): httpx_client.close() def test_client_sets_sdk_header_async(self): - client = FishjamClient( - base_url="https://example.com", - management_token="token456", - ) + client = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) async_client = client.get_async_httpx_client() try: headers = async_client.headers From 3fcf5788a0dbf9e64be4f3486edd59051b8cda4f Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 13:52:48 +0100 Subject: [PATCH 08/18] Revert "fix tests" This reverts commit 645f6caba6472374a4071135d374806f96172758. --- tests/test_room_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 83ed50a..cdf540a 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -56,7 +56,10 @@ def test_valid_token(self): class TestApiVersionHeaders: def test_client_sets_sdk_header_sync(self): - client = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) + client = FishjamClient( + base_url="https://example.com", + management_token="token123", + ) httpx_client = client.get_httpx_client() try: headers = httpx_client.headers @@ -69,7 +72,10 @@ def test_client_sets_sdk_header_sync(self): httpx_client.close() def test_client_sets_sdk_header_async(self): - client = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) + client = FishjamClient( + base_url="https://example.com", + management_token="token456", + ) async_client = client.get_async_httpx_client() try: headers = async_client.headers From d30959a3e1572ac3cd60d00ff862dee5d6e85f69 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 13:53:02 +0100 Subject: [PATCH 09/18] Revert "format" This reverts commit 72c5e82a33485e41752809e8be8af60a29db2131. --- tests/test_room_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index cdf540a..c3aec8f 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -53,7 +53,6 @@ def test_valid_token(self): assert room in all_rooms - class TestApiVersionHeaders: def test_client_sets_sdk_header_sync(self): client = FishjamClient( @@ -70,7 +69,7 @@ def test_client_sets_sdk_header_sync(self): ) finally: httpx_client.close() - + def test_client_sets_sdk_header_async(self): client = FishjamClient( base_url="https://example.com", @@ -86,7 +85,6 @@ def test_client_sets_sdk_header_async(self): ) finally: import asyncio - asyncio.run(async_client.aclose()) From 7b9e95e9636bec20c570c570e553139343e454aa Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 13:53:55 +0100 Subject: [PATCH 10/18] Revert "update jinja and client, move tests to test_room_api.py" This reverts commit 7731e5663e5fdbbc75fa9c777d52b0ada6f5f7a0. --- fishjam/_openapi_client/client.py | 10 ++----- templates/openapi/client.py.jinja | 12 +------- tests/test_openapi_client_headers.py | 41 ++++++++++++++++++++++++++++ tests/test_room_api.py | 34 ----------------------- 4 files changed, 44 insertions(+), 53 deletions(-) create mode 100644 tests/test_openapi_client_headers.py diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index 4b2aac4..64fd976 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -170,8 +170,6 @@ class AuthenticatedClient: 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 - api_prefix: The prefix to use for the api version header - api_header_name: The name of the api version header """ raise_on_unexpected_status: bool = field(default=False, kw_only=True) @@ -194,8 +192,6 @@ class AuthenticatedClient: token: str prefix: str = "Bearer" auth_header_name: str = "Authorization" - api_prefix: str = "python-server" - api_header_name: str = "x-fishjam-api-client" def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": """Get a new client matching this one with additional headers""" @@ -235,8 +231,7 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" - + self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -273,8 +268,7 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" - + self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, diff --git a/templates/openapi/client.py.jinja b/templates/openapi/client.py.jinja index 88cdd6a..a99fc2e 100644 --- a/templates/openapi/client.py.jinja +++ b/templates/openapi/client.py.jinja @@ -18,8 +18,6 @@ from fishjam.version import get_version "token": namespace(type="str", default="", docstring="The token to use for authentication"), "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), - "api_prefix": namespace(type="str", default='"python-server"', docstring="The prefix to use for the api version header"), - "api_header_name": namespace(type="str", default='"x-fishjam-api-client"', docstring="The name of the api version header"), } %} {% macro attr_in_class_docstring(name) %} @@ -183,8 +181,6 @@ class AuthenticatedClient: {{ attr_in_class_docstring("token") | indent(8) }} {{ attr_in_class_docstring("prefix") | indent(8) }} {{ attr_in_class_docstring("auth_header_name") | indent(8) }} - {{ attr_in_class_docstring("api_prefix") | indent(8) }} - {{ attr_in_class_docstring("api_header_name") | indent(8) }} {% endif %} """ @@ -192,12 +188,6 @@ class AuthenticatedClient: {{ declare_attr("token") | indent(4) }} {{ declare_attr("prefix") | indent(4) }} {{ declare_attr("auth_header_name") | indent(4) }} - {{ declare_attr("api_prefix") | indent(4) }} - {{ declare_attr("api_header_name") | indent(4) }} {{ builders("AuthenticatedClient") }} -{% set auth_constructor %} -self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token -self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" -{% endset %} -{{ httpx_stuff("AuthenticatedClient", auth_constructor) }} +{{ httpx_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token; self._headers[\"x-fishjam-api-client\"] = f\"python-server-{get_version()}\"") }} diff --git a/tests/test_openapi_client_headers.py b/tests/test_openapi_client_headers.py new file mode 100644 index 0000000..e58617b --- /dev/null +++ b/tests/test_openapi_client_headers.py @@ -0,0 +1,41 @@ +import pytest + +from fishjam._openapi_client.client import AuthenticatedClient +from fishjam.version import get_version + + +def test_authenticated_client_sets_sdk_and_auth_headers_sync(): + client = AuthenticatedClient( + base_url="https://example.com", + token="token123", + headers={"custom": "value"}, + ) + + httpx_client = client.get_httpx_client() + try: + headers = httpx_client.headers + + assert headers[client.auth_header_name] == "Bearer token123" + assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" + assert headers["custom"] == "value" + finally: + httpx_client.close() + + +@pytest.mark.asyncio +async def test_authenticated_client_sets_sdk_and_auth_headers_async(): + client = AuthenticatedClient( + base_url="https://example.com", + token="token456", + headers={"another": "header"}, + ) + + async_client = client.get_async_httpx_client() + try: + headers = async_client.headers + + assert headers[client.auth_header_name] == "Bearer token456" + assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" + assert headers["another"] == "header" + finally: + await async_client.aclose() diff --git a/tests/test_room_api.py b/tests/test_room_api.py index c3aec8f..3130bbb 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -53,40 +53,6 @@ def test_valid_token(self): assert room in all_rooms -class TestApiVersionHeaders: - def test_client_sets_sdk_header_sync(self): - client = FishjamClient( - base_url="https://example.com", - management_token="token123", - ) - httpx_client = client.get_httpx_client() - try: - headers = httpx_client.headers - - assert ( - headers[client.api_header_name] - == f"{client.api_prefix}-{client.get_sdk_version()}" - ) - finally: - httpx_client.close() - - def test_client_sets_sdk_header_async(self): - client = FishjamClient( - base_url="https://example.com", - management_token="token456", - ) - async_client = client.get_async_httpx_client() - try: - headers = async_client.headers - - assert ( - headers[client.api_header_name] - == f"{client.api_prefix}-{client.get_sdk_version()}" - ) - finally: - import asyncio - asyncio.run(async_client.aclose()) - @pytest.fixture def room_api(): From 2f5fa63d600b5402e99e846c4e9228a76fadc4e7 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 13:54:08 +0100 Subject: [PATCH 11/18] Revert "add autogenerated client" This reverts commit 29eee7d4997c402b4a5e7bf5006b2ca984d56842. --- fishjam/_openapi_client/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index 64fd976..0bf2dd8 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -193,6 +193,9 @@ class AuthenticatedClient: prefix: str = "Bearer" auth_header_name: str = "Authorization" + api_prefix: str = "python-server" + api_header_name: str = "x-fishjam-api-client" + 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: @@ -231,7 +234,7 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" + self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -268,7 +271,7 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-fishjam-api-client"] = f"python-server-{get_version()}" + self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, From 83f9218f59fb439dc23c4fb0b0ba3d628c7b7531 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 13:54:28 +0100 Subject: [PATCH 12/18] Revert "add client headers to template since _clinet.py is autogenerated" This reverts commit a28ab40fbbfaf11f248dc6efe2ea9b89f105edf9. --- templates/openapi/client.py.jinja | 193 ------------------------------ 1 file changed, 193 deletions(-) delete mode 100644 templates/openapi/client.py.jinja diff --git a/templates/openapi/client.py.jinja b/templates/openapi/client.py.jinja deleted file mode 100644 index a99fc2e..0000000 --- a/templates/openapi/client.py.jinja +++ /dev/null @@ -1,193 +0,0 @@ -import ssl -from typing import Any, Union, Optional - -from attrs import define, field, evolve -import httpx - -from fishjam.version import get_version - - -{% set attrs_info = { - "raise_on_unexpected_status": namespace( - type="bool", - default="field(default=False, kw_only=True)", - docstring="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": namespace(type="str", default="", docstring="The token to use for authentication"), - "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), - "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), -} %} - -{% macro attr_in_class_docstring(name) %} -{{ name }}: {{ attrs_info[name].docstring }} -{%- endmacro %} - -{% macro declare_attr(name) %} -{% set attr = attrs_info[name] %} -{{ name }}: {{ attr.type }}{% if attr.default %} = {{ attr.default }}{% endif %} -{% if attr.docstring and config.docstrings_on_attributes +%} -"""{{ attr.docstring }}""" -{%- endif %} -{% endmacro %} - -@define -class Client: - """A class for keeping track of data related to the API - -{% macro httpx_args_docstring() %} - 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. -{% endmacro %} -{{ httpx_args_docstring() }} -{% if not config.docstrings_on_attributes %} - - Attributes: - {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} -{% endif %} - """ -{% macro attributes() %} - {{ declare_attr("raise_on_unexpected_status") | indent(4) }} - _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: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[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: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) -{% endmacro %}{{ attributes() }} -{% macro builders(self) %} - def with_headers(self, headers: dict[str, str]) -> "{{ self }}": - """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]) -> "{{ self }}": - """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) -> "{{ self }}": - """Get a new client matching this one with a new timeout (in seconds)""" - 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) -{% endmacro %}{{ builders("Client") }} -{% macro httpx_stuff(name, custom_constructor=None) %} - def set_httpx_client(self, client: httpx.Client) -> "{{ name }}": - """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: - {% if custom_constructor %} - {{ custom_constructor | indent(12) }} - {% endif %} - 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) -> "{{ name }}": - """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) -> "{{ name }}": - """Manually 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: - {% if custom_constructor %} - {{ custom_constructor | indent(12) }} - {% endif %} - 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) -> "{{ name }}": - """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) -{% endmacro %}{{ httpx_stuff("Client") }} - -@define -class AuthenticatedClient: - """A Client which has been authenticated for use on secured endpoints - -{{ httpx_args_docstring() }} -{% if not config.docstrings_on_attributes %} - - Attributes: - {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} - {{ attr_in_class_docstring("token") | indent(8) }} - {{ attr_in_class_docstring("prefix") | indent(8) }} - {{ attr_in_class_docstring("auth_header_name") | indent(8) }} -{% endif %} - """ - -{{ attributes() }} - {{ declare_attr("token") | indent(4) }} - {{ declare_attr("prefix") | indent(4) }} - {{ declare_attr("auth_header_name") | indent(4) }} - -{{ builders("AuthenticatedClient") }} -{{ httpx_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token; self._headers[\"x-fishjam-api-client\"] = f\"python-server-{get_version()}\"") }} From 3f663897fffa6fb77a1ec5a412ec41103796a82f Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 14:35:28 +0100 Subject: [PATCH 13/18] Revert "comment suggestions, tests" This reverts commit 80f2b287e3a0e879c5d4ecd8a9aaf3918eeba5b9. --- fishjam/_openapi_client/client.py | 7 ++--- tests/test_openapi_client_headers.py | 41 ---------------------------- 2 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 tests/test_openapi_client_headers.py diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index 0bf2dd8..eb4e23c 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -193,9 +193,6 @@ class AuthenticatedClient: prefix: str = "Bearer" auth_header_name: str = "Authorization" - api_prefix: str = "python-server" - api_header_name: str = "x-fishjam-api-client" - 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: @@ -234,7 +231,7 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" + self._headers["x-sdk_version"] = f"py-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -271,7 +268,7 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers[self.api_header_name] = f"{self.api_prefix}-{get_version()}" + self._headers["x-sdk_version"] = f"py-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, diff --git a/tests/test_openapi_client_headers.py b/tests/test_openapi_client_headers.py deleted file mode 100644 index e58617b..0000000 --- a/tests/test_openapi_client_headers.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest - -from fishjam._openapi_client.client import AuthenticatedClient -from fishjam.version import get_version - - -def test_authenticated_client_sets_sdk_and_auth_headers_sync(): - client = AuthenticatedClient( - base_url="https://example.com", - token="token123", - headers={"custom": "value"}, - ) - - httpx_client = client.get_httpx_client() - try: - headers = httpx_client.headers - - assert headers[client.auth_header_name] == "Bearer token123" - assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" - assert headers["custom"] == "value" - finally: - httpx_client.close() - - -@pytest.mark.asyncio -async def test_authenticated_client_sets_sdk_and_auth_headers_async(): - client = AuthenticatedClient( - base_url="https://example.com", - token="token456", - headers={"another": "header"}, - ) - - async_client = client.get_async_httpx_client() - try: - headers = async_client.headers - - assert headers[client.auth_header_name] == "Bearer token456" - assert headers[client.api_header_name] == f"{client.api_prefix}-{get_version()}" - assert headers["another"] == "header" - finally: - await async_client.aclose() From e7e7243511188a154da135f026fecfad10f7ad15 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 14:35:47 +0100 Subject: [PATCH 14/18] Revert "sent sdk version in headers" This reverts commit cf9c640409ce0f6789408119163ea58c04dafb5d. --- fishjam/_openapi_client/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fishjam/_openapi_client/client.py b/fishjam/_openapi_client/client.py index eb4e23c..eeffd00 100644 --- a/fishjam/_openapi_client/client.py +++ b/fishjam/_openapi_client/client.py @@ -4,8 +4,6 @@ import httpx from attrs import define, evolve, field -from fishjam.version import get_version - @define class Client: @@ -231,7 +229,6 @@ def get_httpx_client(self) -> httpx.Client: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-sdk_version"] = f"py-{get_version()}" self._client = httpx.Client( base_url=self._base_url, cookies=self._cookies, @@ -268,7 +265,6 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: self._headers[self.auth_header_name] = ( f"{self.prefix} {self.token}" if self.prefix else self.token ) - self._headers["x-sdk_version"] = f"py-{get_version()}" self._async_client = httpx.AsyncClient( base_url=self._base_url, cookies=self._cookies, From b8016b7e622ade4894c7cfd183160f47540100a6 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 14:37:41 +0100 Subject: [PATCH 15/18] add headers to _client.py since client.py is autogenerated --- fishjam/api/_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index c55c58c..79556c4 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -5,12 +5,13 @@ from fishjam._openapi_client.types import Response from fishjam.errors import HTTPError from fishjam.utils import get_fishjam_url +from fishjam.version import get_version class Client: def __init__(self, fishjam_id: str, management_token: str): self._fishjam_url = get_fishjam_url(fishjam_id) - self.client = AuthenticatedClient(self._fishjam_url, token=management_token) + self.client = AuthenticatedClient(self._fishjam_url, token=management_token, headers={"x-fishjam-api-client": f"python-server-{get_version()}"}) def _request(self, method, **kwargs): response = method.sync_detailed(client=self.client, **kwargs) From 467eaeebf4e81db4a67e3e5d46c6c2da391d44b2 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 14:45:25 +0100 Subject: [PATCH 16/18] add test --- fishjam/api/_client.py | 6 +++++- tests/test_room_api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index 79556c4..fdfd0e2 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -11,7 +11,11 @@ class Client: def __init__(self, fishjam_id: str, management_token: str): self._fishjam_url = get_fishjam_url(fishjam_id) - self.client = AuthenticatedClient(self._fishjam_url, token=management_token, headers={"x-fishjam-api-client": f"python-server-{get_version()}"}) + self.client = AuthenticatedClient( + self._fishjam_url, + token=management_token, + headers={"x-fishjam-api-client": f"python-server-{get_version()}"}, + ) def _request(self, method, **kwargs): response = method.sync_detailed(client=self.client, **kwargs) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 3130bbb..8d38103 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -1,5 +1,7 @@ import os +from unittest.mock import Mock, patch +import httpx import pytest from fishjam import ( @@ -26,6 +28,7 @@ RoomType, VideoCodec, ) +from fishjam.version import get_version HOST = "proxy" if os.getenv("DOCKER_TEST") == "TRUE" else "localhost" FISHJAM_ID = f"http://{HOST}:5555" @@ -54,6 +57,36 @@ def test_valid_token(self): assert room in all_rooms +class TestAPIClientHeader: + def test_x_fishjam_api_client_header_is_sent(self): + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = httpx.Headers({}) + mock_response.json.return_value = {"data": []} + + captured_headers = None + + def mock_send(request, **kwargs): + nonlocal captured_headers + captured_headers = dict(request.headers) + return mock_response + + room_api = FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) + + with patch.object(httpx.HTTPTransport, "handle_request", side_effect=mock_send): + try: + room_api.get_all_rooms() + except Exception: + # We don't care if the request fails, we just want to check the headers + pass + + assert captured_headers is not None + assert "x-fishjam-api-client" in captured_headers + + expected_header_value = f"python-server-{get_version()}" + assert captured_headers["x-fishjam-api-client"] == expected_header_value + + @pytest.fixture def room_api(): return FishjamClient(FISHJAM_ID, MANAGEMENT_TOKEN) From 4a2a6043db79c1da8beda676de86926504750e1b Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Tue, 20 Jan 2026 14:57:36 +0100 Subject: [PATCH 17/18] after changing fishjam api validation, max_peers can also be string like: "10" because cast value will automatically check if string is a valid positive number --- tests/test_room_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 8d38103..e5a79ab 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -135,7 +135,7 @@ def test_valid_params(self, room_api: FishjamClient): assert room in room_api.get_all_rooms() def test_invalid_max_peers(self, room_api: FishjamClient): - options = RoomOptions(max_peers="10") + options = RoomOptions(max_peers="nan") with pytest.raises(BadRequestError): room_api.create_room(options) From 1b9b8ceda3333c7925c48c0b5ae307c9b2fb135c Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 21 Jan 2026 11:46:14 +0100 Subject: [PATCH 18/18] update header_value to new schema --- fishjam/api/_client.py | 2 +- tests/test_room_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index fdfd0e2..c73fc61 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -14,7 +14,7 @@ def __init__(self, fishjam_id: str, management_token: str): self.client = AuthenticatedClient( self._fishjam_url, token=management_token, - headers={"x-fishjam-api-client": f"python-server-{get_version()}"}, + headers={"x-fishjam-api-client": f"python-server/{get_version()}"}, ) def _request(self, method, **kwargs): diff --git a/tests/test_room_api.py b/tests/test_room_api.py index e5a79ab..72f9c85 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -83,7 +83,7 @@ def mock_send(request, **kwargs): assert captured_headers is not None assert "x-fishjam-api-client" in captured_headers - expected_header_value = f"python-server-{get_version()}" + expected_header_value = f"python-server/{get_version()}" assert captured_headers["x-fishjam-api-client"] == expected_header_value