From dea211b4a21c1124b19128a844fb6bbaf66703bb Mon Sep 17 00:00:00 2001 From: Ray Date: Sun, 19 Jul 2026 02:21:27 +0800 Subject: [PATCH] fix: resolve BASE_URL at request time to restore 0.2.x override semantics PageIndexClient snapshotted BASE_URL into CloudBackend and LegacyCloudAPI at construction, so reassigning client.BASE_URL afterwards was silently ignored and requests kept going to the default endpoint. Pass a callable resolved per request instead, so instance- and class-level reassignment after construction work again alongside the pre-construction override. Claude-Session: https://claude.ai/code/session_014B4HZkjdSiZXDmJtH5Jexn --- pageindex/backend/cloud.py | 14 ++++++++++---- pageindex/client.py | 7 +++++-- pageindex/cloud_api.py | 16 +++++++++++++--- tests/test_cloud_backend.py | 2 +- tests/test_legacy_sdk_contract.py | 17 +++++++++++++++++ 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/pageindex/backend/cloud.py b/pageindex/backend/cloud.py index f97e77a..1339a79 100644 --- a/pageindex/backend/cloud.py +++ b/pageindex/backend/cloud.py @@ -11,7 +11,7 @@ import re import time import urllib.parse import requests -from typing import AsyncIterator +from typing import AsyncIterator, Callable from ..cloud_api import API_BASE # single source of truth for the cloud base URL from ..errors import (AUTH_HINT, CloudAPIError, CollectionNotFoundError, @@ -31,13 +31,19 @@ def _as_int(value): class CloudBackend: - def __init__(self, api_key: str, base_url: str | None = None): + def __init__(self, api_key: str, base_url: str | Callable[[], str] | None = None): self._api_key = api_key self._base_url = base_url or API_BASE self._headers = {"api_key": api_key} self._folder_id_cache: dict[str, str | None] = {} self._folder_warning_shown = False + @property + def base_url(self) -> str: + # A callable is resolved per request so reassigning client.BASE_URL + # after construction takes effect, matching 0.2.x call-time semantics. + return self._base_url() if callable(self._base_url) else self._base_url + # ── HTTP helpers ────────────────────────────────────────────────────── # Folder API statuses meaning "folders are not available on this account" @@ -61,7 +67,7 @@ class CloudBackend: """HTTP helper. ``retries`` caps total attempts — pass 1 for non-idempotent, expensive calls (e.g. chat completions) where a retry would redo the full server-side work.""" - url = f"{self._base_url}{path}" + url = f"{self.base_url}{path}" kwargs.setdefault("timeout", 30) last_status: int | None = None for attempt in range(retries): @@ -430,7 +436,7 @@ class CloudBackend: if not doc_id: raise ValueError("collection has no documents to query") headers = self._headers - base_url = self._base_url + base_url = self.base_url # Queue carries QueryEvent, an Exception to re-raise, or None (end). queue: asyncio.Queue[QueryEvent | Exception | None] = asyncio.Queue() loop = asyncio.get_running_loop() diff --git a/pageindex/client.py b/pageindex/client.py index 362bb9a..dfe4433 100644 --- a/pageindex/client.py +++ b/pageindex/client.py @@ -74,8 +74,11 @@ class PageIndexClient: def _init_cloud(self, api_key: str): from .backend.cloud import CloudBackend from .cloud_api import LegacyCloudAPI - self._backend = CloudBackend(api_key=api_key, base_url=self.BASE_URL) - self._legacy_cloud_api = LegacyCloudAPI(api_key=api_key, base_url=self.BASE_URL) + # Pass a callable so BASE_URL is re-read on every request — 0.2.x + # allowed reassigning client.BASE_URL after construction. + base_url = lambda: self.BASE_URL + self._backend = CloudBackend(api_key=api_key, base_url=base_url) + self._legacy_cloud_api = LegacyCloudAPI(api_key=api_key, base_url=base_url) def _init_local(self, model: str = None, retrieve_model: str = None, storage_path: str = None, storage=None, diff --git a/pageindex/cloud_api.py b/pageindex/cloud_api.py index 109141e..ec8971f 100644 --- a/pageindex/cloud_api.py +++ b/pageindex/cloud_api.py @@ -2,7 +2,7 @@ from __future__ import annotations import json import urllib.parse -from typing import Any, Iterator +from typing import Any, Callable, Iterator import requests @@ -19,9 +19,19 @@ class LegacyCloudAPI: BASE_URL = API_BASE - def __init__(self, api_key: str, base_url: str | None = None): + def __init__(self, api_key: str, base_url: str | Callable[[], str] | None = None): self.api_key = api_key - self.base_url = base_url or self.BASE_URL + self._base_url = base_url or self.BASE_URL + + @property + def base_url(self) -> str: + # A callable is resolved per request so reassigning client.BASE_URL + # after construction takes effect, matching 0.2.x call-time semantics. + return self._base_url() if callable(self._base_url) else self._base_url + + @base_url.setter + def base_url(self, value: str | Callable[[], str]) -> None: + self._base_url = value @staticmethod def _enc(value: str) -> str: diff --git a/tests/test_cloud_backend.py b/tests/test_cloud_backend.py index d023667..1fa21b0 100644 --- a/tests/test_cloud_backend.py +++ b/tests/test_cloud_backend.py @@ -137,7 +137,7 @@ def test_client_base_url_override_reaches_both_backends(): BASE_URL = "https://staging.example.com" client = StagingClient(api_key="pi-test") - assert client._backend._base_url == "https://staging.example.com" + assert client._backend.base_url == "https://staging.example.com" assert client._legacy_cloud_api.base_url == "https://staging.example.com" diff --git a/tests/test_legacy_sdk_contract.py b/tests/test_legacy_sdk_contract.py index 6abab5d..612ca65 100644 --- a/tests/test_legacy_sdk_contract.py +++ b/tests/test_legacy_sdk_contract.py @@ -84,6 +84,23 @@ def test_legacy_base_url_can_be_overridden_from_client(monkeypatch): assert calls[0]["headers"] == {"api_key": "pi-test"} +def test_legacy_base_url_reassignment_after_construction(monkeypatch): + calls = [] + + def fake_request(method, url, headers=None, **kwargs): + calls.append({"method": method, "url": url}) + return FakeResponse(payload={"id": "doc-1"}) + + monkeypatch.setattr("pageindex.cloud_api.requests.request", fake_request) + + client = PageIndexClient("pi-test") + client.BASE_URL = "https://staging.pageindex.test" + client.get_document("doc-1") + + assert calls[0]["url"] == "https://staging.pageindex.test/doc/doc-1/metadata/" + assert client._backend.base_url == "https://staging.pageindex.test" + + def test_submit_document_uses_legacy_endpoint(monkeypatch, tmp_path): calls = []