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
This commit is contained in:
Ray 2026-07-19 02:21:27 +08:00
parent 3a1727b578
commit dea211b4a2
5 changed files with 46 additions and 10 deletions

View file

@ -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()

View file

@ -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,

View file

@ -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:

View file

@ -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"

View file

@ -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 = []