From 20f5ce522b56d676f7e5988077555bd0cbe67606 Mon Sep 17 00:00:00 2001 From: Chris Freeman Date: Fri, 27 Feb 2026 12:25:08 -0700 Subject: [PATCH 1/3] Add server URL normalization hook Prepends https:// to server URLs when no scheme is provided and strips trailing slashes, improving developer experience for SDK initialization. --- src/glean/api_client/_hooks/registration.py | 4 ++ .../_hooks/server_url_normalizer.py | 21 ++++++ tests/test_server_url_normalizer.py | 71 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/glean/api_client/_hooks/server_url_normalizer.py create mode 100644 tests/test_server_url_normalizer.py diff --git a/src/glean/api_client/_hooks/registration.py b/src/glean/api_client/_hooks/registration.py index a064445e..8d8610f1 100644 --- a/src/glean/api_client/_hooks/registration.py +++ b/src/glean/api_client/_hooks/registration.py @@ -1,4 +1,5 @@ from .types import Hooks +from .server_url_normalizer import ServerURLNormalizerHook from .multipart_fix_hook import MultipartFileFieldFixHook from .agent_file_upload_error_hook import AgentFileUploadErrorHook from .x_glean import XGlean @@ -15,6 +16,9 @@ def init_hooks(hooks: Hooks): with an instance of a hook that implements that specific Hook interface Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance""" + # Register hook to normalize server URLs (prepend https:// if no scheme provided) + hooks.register_sdk_init_hook(ServerURLNormalizerHook()) + # Register hook to fix multipart file field names that incorrectly have '[]' suffix hooks.register_sdk_init_hook(MultipartFileFieldFixHook()) diff --git a/src/glean/api_client/_hooks/server_url_normalizer.py b/src/glean/api_client/_hooks/server_url_normalizer.py new file mode 100644 index 00000000..42561988 --- /dev/null +++ b/src/glean/api_client/_hooks/server_url_normalizer.py @@ -0,0 +1,21 @@ +"""Hook to normalize server URLs, prepending https:// if no scheme is provided.""" + +import re +from typing import Tuple +from .types import SDKInitHook +from glean.api_client.httpclient import HttpClient + + +def normalize_server_url(url: str) -> str: + normalized = url + if not re.match(r'^https?://', normalized, re.IGNORECASE): + normalized = f'https://{normalized}' + normalized = normalized.rstrip('/') + return normalized + + +class ServerURLNormalizerHook(SDKInitHook): + """Normalizes server URLs by prepending https:// if no scheme is provided.""" + + def sdk_init(self, base_url: str, client: HttpClient) -> Tuple[str, HttpClient]: + return normalize_server_url(base_url), client diff --git a/tests/test_server_url_normalizer.py b/tests/test_server_url_normalizer.py new file mode 100644 index 00000000..ef9f8696 --- /dev/null +++ b/tests/test_server_url_normalizer.py @@ -0,0 +1,71 @@ +"""Tests for the server URL normalizer hook.""" + +from unittest.mock import Mock + +import pytest + +from src.glean.api_client._hooks.server_url_normalizer import ( + ServerURLNormalizerHook, + normalize_server_url, +) +from src.glean.api_client.httpclient import HttpClient + + +class TestNormalizeServerUrl: + """Test cases for the normalize_server_url function.""" + + def test_no_scheme_prepends_https(self): + assert normalize_server_url("example.glean.com") == "https://example.glean.com" + + def test_https_preserved(self): + assert normalize_server_url("https://example.glean.com") == "https://example.glean.com" + + def test_http_localhost_preserved(self): + assert normalize_server_url("http://localhost:8080") == "http://localhost:8080" + + def test_http_non_localhost_preserved(self): + assert normalize_server_url("http://example.glean.com") == "http://example.glean.com" + + def test_trailing_slash_stripped(self): + assert normalize_server_url("https://example.glean.com/") == "https://example.glean.com" + + def test_multiple_trailing_slashes_stripped(self): + assert normalize_server_url("https://example.glean.com///") == "https://example.glean.com" + + def test_no_scheme_with_trailing_slash(self): + assert normalize_server_url("example.glean.com/") == "https://example.glean.com" + + def test_url_with_path(self): + assert normalize_server_url("https://example.glean.com/api/v1") == "https://example.glean.com/api/v1" + + def test_url_with_path_and_trailing_slash(self): + assert normalize_server_url("https://example.glean.com/api/v1/") == "https://example.glean.com/api/v1" + + def test_no_scheme_with_path(self): + assert normalize_server_url("example.glean.com/api/v1") == "https://example.glean.com/api/v1" + + def test_case_insensitive_scheme(self): + assert normalize_server_url("HTTPS://example.glean.com") == "HTTPS://example.glean.com" + assert normalize_server_url("HTTP://localhost") == "HTTP://localhost" + + +class TestServerURLNormalizerHook: + """Test cases for the ServerURLNormalizerHook.""" + + def setup_method(self): + self.hook = ServerURLNormalizerHook() + self.mock_client = Mock(spec=HttpClient) + + def test_sdk_init_normalizes_url(self): + result_url, result_client = self.hook.sdk_init("example.glean.com", self.mock_client) + assert result_url == "https://example.glean.com" + assert result_client == self.mock_client + + def test_sdk_init_preserves_client(self): + result_url, result_client = self.hook.sdk_init("https://example.glean.com", self.mock_client) + assert result_url == "https://example.glean.com" + assert result_client is self.mock_client + + +if __name__ == "__main__": + pytest.main([__file__]) From 17f3736e55e0a8498d5247d06329be80f59973ad Mon Sep 17 00:00:00 2001 From: Chris Freeman Date: Wed, 4 Mar 2026 09:48:41 -0700 Subject: [PATCH 2/3] docs: update README to promote server_url over instance Replace all code examples and documentation to use server_url as the primary configuration parameter. The instance parameter is preserved for backwards compatibility but deprioritized in documentation. --- README.md | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0e9dcb12..1e8087b2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Each namespace has its own authentication requirements and access patterns. Whil from glean.api_client import Glean import os -with Glean(api_token="client-token", instance="instance-name") as glean: +with Glean(api_token="client-token", server_url="mycompany-be.glean.com") as glean: search_response = glean.client.search.query(query="search term") print(search_response) @@ -26,7 +26,7 @@ with Glean(api_token="client-token", instance="instance-name") as glean: from glean.api_client import Glean, models import os -with Glean(api_token="indexing-token", instance="instance-name") as glean: +with Glean(api_token="indexing-token", server_url="mycompany-be.glean.com") as glean: document_response = glean.indexing.documents.index( document=models.Document( id="doc-123", @@ -744,14 +744,34 @@ By default, an API error will raise a errors.GleanError exception, which has the ## Server Selection -### Server Variables +### Server URL -The default server `https://{instance}-be.glean.com` contains variables and is set to `https://instance-name-be.glean.com` by default. To override default values, the following parameters are available when initializing the SDK client instance: +The recommended way to configure the Glean API server is to pass `server_url` when initializing the SDK client: + +```python +from glean.api_client import Glean + +glean = Glean( + api_token="your-api-token", + server_url="mycompany-be.glean.com", +) +``` + +The SDK accepts schemeless URLs (e.g., `"mycompany-be.glean.com"`); the `https://` prefix is added automatically. + +You can also configure the server URL via the `GLEAN_SERVER_URL` environment variable. + +### Server Variables (backwards compatible) + +For backwards compatibility, the SDK also supports the `instance` parameter, which constructs the server URL using the pattern `https://{instance}-be.glean.com`: | Variable | Parameter | Default | Description | | ---------- | --------------- | ----------------- | ------------------------------------------------------------------------------------------------------ | | `instance` | `instance: str` | `"instance-name"` | The instance name (typically the email domain without the TLD) that determines the deployment backend. | +> [!NOTE] +> The `server_url` parameter is preferred over `instance`. If both are provided, `server_url` takes precedence. + #### Example ```python @@ -761,8 +781,7 @@ import os with Glean( - server_idx=0, - instance="instance-name", + server_url="mycompany-be.glean.com", api_token=os.getenv("GLEAN_API_TOKEN", ""), ) as glean: @@ -989,7 +1008,7 @@ from glean.api_client import Glean glean = Glean( api_token=os.environ.get("GLEAN_API_TOKEN", ""), - instance=os.environ.get("GLEAN_INSTANCE", ""), + server_url="mycompany-be.glean.com", ) ``` @@ -1002,7 +1021,7 @@ from glean.api_client import Glean glean = Glean( api_token=os.environ.get("GLEAN_API_TOKEN", ""), - instance=os.environ.get("GLEAN_INSTANCE", ""), + server_url="mycompany-be.glean.com", exclude_deprecated_after="2026-10-15", include_experimental=True, ) From cd54824426c08ecc1e42dfc5ab480b11c849a177 Mon Sep 17 00:00:00 2001 From: Chris Freeman Date: Wed, 4 Mar 2026 10:01:16 -0700 Subject: [PATCH 3/3] fix: use fully qualified https:// URLs in documentation examples --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1e8087b2..e1f3d1fa 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Each namespace has its own authentication requirements and access patterns. Whil from glean.api_client import Glean import os -with Glean(api_token="client-token", server_url="mycompany-be.glean.com") as glean: +with Glean(api_token="client-token", server_url="https://mycompany-be.glean.com") as glean: search_response = glean.client.search.query(query="search term") print(search_response) @@ -26,7 +26,7 @@ with Glean(api_token="client-token", server_url="mycompany-be.glean.com") as gle from glean.api_client import Glean, models import os -with Glean(api_token="indexing-token", server_url="mycompany-be.glean.com") as glean: +with Glean(api_token="indexing-token", server_url="https://mycompany-be.glean.com") as glean: document_response = glean.indexing.documents.index( document=models.Document( id="doc-123", @@ -753,11 +753,11 @@ from glean.api_client import Glean glean = Glean( api_token="your-api-token", - server_url="mycompany-be.glean.com", + server_url="https://mycompany-be.glean.com", ) ``` -The SDK accepts schemeless URLs (e.g., `"mycompany-be.glean.com"`); the `https://` prefix is added automatically. +Schemeless URLs (e.g., `"mycompany-be.glean.com"`) are also accepted — the SDK will automatically prepend `https://`. You can also configure the server URL via the `GLEAN_SERVER_URL` environment variable. @@ -781,7 +781,7 @@ import os with Glean( - server_url="mycompany-be.glean.com", + server_url="https://mycompany-be.glean.com", api_token=os.getenv("GLEAN_API_TOKEN", ""), ) as glean: @@ -1008,7 +1008,7 @@ from glean.api_client import Glean glean = Glean( api_token=os.environ.get("GLEAN_API_TOKEN", ""), - server_url="mycompany-be.glean.com", + server_url="https://mycompany-be.glean.com", ) ``` @@ -1021,7 +1021,7 @@ from glean.api_client import Glean glean = Glean( api_token=os.environ.get("GLEAN_API_TOKEN", ""), - server_url="mycompany-be.glean.com", + server_url="https://mycompany-be.glean.com", exclude_deprecated_after="2026-10-15", include_experimental=True, )