diff --git a/src/auth/src/supabase_auth/_async/gotrue_client.py b/src/auth/src/supabase_auth/_async/gotrue_client.py index 4048f93a..5ee1670b 100644 --- a/src/auth/src/supabase_auth/_async/gotrue_client.py +++ b/src/auth/src/supabase_auth/_async/gotrue_client.py @@ -113,11 +113,13 @@ def __init__( proxy: Optional[str] = None, ) -> None: extra_headers = { - "X-Client-Info": f"supabase-py/supabase_auth v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_auth v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), } if headers: extra_headers.update(headers) diff --git a/src/auth/src/supabase_auth/_sync/gotrue_client.py b/src/auth/src/supabase_auth/_sync/gotrue_client.py index 882950e7..063d1ddb 100644 --- a/src/auth/src/supabase_auth/_sync/gotrue_client.py +++ b/src/auth/src/supabase_auth/_sync/gotrue_client.py @@ -113,11 +113,13 @@ def __init__( proxy: Optional[str] = None, ) -> None: extra_headers = { - "X-Client-Info": f"supabase-py/supabase_auth v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_auth v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), } if headers: extra_headers.update(headers) diff --git a/src/functions/src/supabase_functions/_async/functions_client.py b/src/functions/src/supabase_functions/_async/functions_client.py index f7958deb..2f7529b1 100644 --- a/src/functions/src/supabase_functions/_async/functions_client.py +++ b/src/functions/src/supabase_functions/_async/functions_client.py @@ -29,11 +29,13 @@ def __init__( raise ValueError("url must be a valid HTTP URL string") self.url = URL(url) self.headers = { - "X-Client-Info": f"supabase-py/supabase_functions v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_functions v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/functions/src/supabase_functions/_sync/functions_client.py b/src/functions/src/supabase_functions/_sync/functions_client.py index 65268592..18f073d7 100644 --- a/src/functions/src/supabase_functions/_sync/functions_client.py +++ b/src/functions/src/supabase_functions/_sync/functions_client.py @@ -29,11 +29,13 @@ def __init__( raise ValueError("url must be a valid HTTP URL string") self.url = URL(url) self.headers = { - "X-Client-Info": f"supabase-py/supabase_functions v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/supabase_functions v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/functions/tests/_async/test_function_client.py b/src/functions/tests/_async/test_function_client.py index c991aed2..a821deb8 100644 --- a/src/functions/tests/_async/test_function_client.py +++ b/src/functions/tests/_async/test_function_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict from unittest.mock import AsyncMock, Mock, patch @@ -36,9 +37,9 @@ async def test_init_with_valid_params( ) assert str(client.url) == valid_url assert "X-Client-Info" in client.headers - assert ( - client.headers["X-Client-Info"] - == f"supabase-py/supabase_functions v{__version__}" + assert re.match( + rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", + client.headers["X-Client-Info"], ) assert client._client.timeout == Timeout(10) diff --git a/src/functions/tests/_sync/test_function_client.py b/src/functions/tests/_sync/test_function_client.py index 469f7d36..6be348df 100644 --- a/src/functions/tests/_sync/test_function_client.py +++ b/src/functions/tests/_sync/test_function_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict from unittest.mock import Mock, patch @@ -36,9 +37,9 @@ def test_init_with_valid_params( ) assert str(client.url) == valid_url assert "X-Client-Info" in client.headers - assert ( - client.headers["X-Client-Info"] - == f"supabase-py/supabase_functions v{__version__}" + assert re.match( + rf"^supabase-py/supabase_functions v{re.escape(__version__)}; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", + client.headers["X-Client-Info"], ) assert client._client.timeout == Timeout(10) diff --git a/src/functions/tests/test_client.py b/src/functions/tests/test_client.py index 80a200e4..2165929a 100644 --- a/src/functions/tests/test_client.py +++ b/src/functions/tests/test_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict import pytest @@ -14,6 +15,33 @@ def valid_headers() -> Dict[str, str]: return {"Authorization": "Bearer test_token", "Content-Type": "application/json"} +_X_CLIENT_INFO_PATTERN = re.compile( + r"^supabase-py/supabase_functions v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$" +) + + +def test_async_x_client_info_structured_format( + valid_url: str, valid_headers: Dict[str, str] +) -> None: + client = AsyncFunctionsClient(url=valid_url, headers=valid_headers) + x_client_info = client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + +def test_sync_x_client_info_structured_format( + valid_url: str, valid_headers: Dict[str, str] +) -> None: + client = SyncFunctionsClient(url=valid_url, headers=valid_headers) + x_client_info = client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + def test_create_async_client(valid_url: str, valid_headers: Dict[str, str]) -> None: # Test creating async client with explicit verify=True client = create_client( diff --git a/src/postgrest/src/postgrest/_async/client.py b/src/postgrest/src/postgrest/_async/client.py index 00962e0b..b85e5895 100644 --- a/src/postgrest/src/postgrest/_async/client.py +++ b/src/postgrest/src/postgrest/_async/client.py @@ -38,11 +38,13 @@ def __init__( http_client: Optional[AsyncClient] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/postgrest-py v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/postgrest-py v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/postgrest/src/postgrest/_sync/client.py b/src/postgrest/src/postgrest/_sync/client.py index 14f48ea7..58b2e8b0 100644 --- a/src/postgrest/src/postgrest/_sync/client.py +++ b/src/postgrest/src/postgrest/_sync/client.py @@ -38,11 +38,13 @@ def __init__( http_client: Optional[Client] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/postgrest-py v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/postgrest-py v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/postgrest/tests/_async/test_client.py b/src/postgrest/tests/_async/test_client.py index 349a0345..0940cbf1 100644 --- a/src/postgrest/tests/_async/test_client.py +++ b/src/postgrest/tests/_async/test_client.py @@ -1,3 +1,4 @@ +import re from unittest.mock import patch import pytest @@ -22,6 +23,16 @@ async def postgrest_client(): yield client +class TestXClientInfo: + def test_structured_metadata_format(self, postgrest_client: AsyncPostgrestClient): + x_client_info = postgrest_client.session.headers.get("X-Client-Info") + assert x_client_info is not None + assert re.match( + r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", + x_client_info, + ), f"X-Client-Info format is wrong: {x_client_info}" + + class TestConstructor: def test_simple(self, postgrest_client: AsyncPostgrestClient): session = postgrest_client.session diff --git a/src/postgrest/tests/_sync/test_client.py b/src/postgrest/tests/_sync/test_client.py index 9a7f117c..08955089 100644 --- a/src/postgrest/tests/_sync/test_client.py +++ b/src/postgrest/tests/_sync/test_client.py @@ -1,3 +1,4 @@ +import re from unittest.mock import patch import pytest @@ -22,6 +23,16 @@ def postgrest_client(): yield client +class TestXClientInfo: + def test_structured_metadata_format(self, postgrest_client: SyncPostgrestClient): + x_client_info = postgrest_client.session.headers.get("X-Client-Info") + assert x_client_info is not None + assert re.match( + r"^supabase-py/postgrest-py v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$", + x_client_info, + ), f"X-Client-Info format is wrong: {x_client_info}" + + class TestConstructor: def test_simple(self, postgrest_client: SyncPostgrestClient): session = postgrest_client.session diff --git a/src/storage/src/storage3/_async/client.py b/src/storage/src/storage3/_async/client.py index 2caec5ef..322619d7 100644 --- a/src/storage/src/storage3/_async/client.py +++ b/src/storage/src/storage3/_async/client.py @@ -34,11 +34,13 @@ def __init__( http_client: Optional[AsyncClient] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/storage3 v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/storage3 v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/storage/src/storage3/_sync/client.py b/src/storage/src/storage3/_sync/client.py index 956ede79..e262d4dd 100644 --- a/src/storage/src/storage3/_sync/client.py +++ b/src/storage/src/storage3/_sync/client.py @@ -34,11 +34,13 @@ def __init__( http_client: Optional[Client] = None, ) -> None: headers = { - "X-Client-Info": f"supabase-py/storage3 v{__version__}", - "X-Supabase-Client-Platform": platform.system(), - "X-Supabase-Client-Platform-Version": platform.release(), - "X-Supabase-Client-Runtime": "python", - "X-Supabase-Client-Runtime-Version": platform.python_version(), + "X-Client-Info": ( + f"supabase-py/storage3 v{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ), **headers, } diff --git a/src/storage/tests/test_client.py b/src/storage/tests/test_client.py index 4d926cc6..67535f72 100644 --- a/src/storage/tests/test_client.py +++ b/src/storage/tests/test_client.py @@ -1,3 +1,4 @@ +import re from typing import Dict import pytest @@ -16,6 +17,29 @@ def valid_headers() -> Dict[str, str]: return {"Authorization": "Bearer test_token", "apikey": "test_api_key"} +_X_CLIENT_INFO_PATTERN = re.compile( + r"^supabase-py/storage3 v[\d.]+; platform=.+; platform-version=.+; runtime=python; runtime-version=\S+$" +) + + +def test_async_x_client_info_structured_format(valid_url, valid_headers) -> None: + client = AsyncStorageClient(url=valid_url, headers=valid_headers) + x_client_info = client._client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + +def test_sync_x_client_info_structured_format(valid_url, valid_headers) -> None: + client = SyncStorageClient(url=valid_url, headers=valid_headers) + x_client_info = client._client.headers.get("X-Client-Info") + assert x_client_info is not None + assert _X_CLIENT_INFO_PATTERN.match( + x_client_info + ), f"X-Client-Info format is wrong: {x_client_info}" + + def test_create_async_client(valid_url, valid_headers) -> None: client = AsyncStorageClient(url=valid_url, headers=valid_headers) diff --git a/src/supabase/src/supabase/lib/client_options.py b/src/supabase/src/supabase/lib/client_options.py index 44450c0e..99ea1b42 100644 --- a/src/supabase/src/supabase/lib/client_options.py +++ b/src/supabase/src/supabase/lib/client_options.py @@ -1,3 +1,4 @@ +import platform from dataclasses import dataclass, field from typing import Dict, Optional, Union @@ -19,7 +20,15 @@ from ..version import __version__ -DEFAULT_HEADERS = {"X-Client-Info": f"supabase-py/{__version__}"} +DEFAULT_HEADERS = { + "X-Client-Info": ( + f"supabase-py/{__version__}" + f"; platform={platform.system()}" + f"; platform-version={platform.release()}" + f"; runtime=python" + f"; runtime-version={platform.python_version()}" + ) +} @dataclass