Skip to content

Commit 784c952

Browse files
committed
refactor(auth): rename PAT credential to pat
Rename the PAT-credential parameter/attribute from access_token to pat. The env var was already QODER_PAT. Vault MCP OAuth access_token params are unaffected.
1 parent 6e7689f commit 784c952

7 files changed

Lines changed: 37 additions & 49 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Explicit arguments take precedence over environment variables. The clients never
5454

5555
```python
5656
client = Forward(
57-
access_token="your-access-token",
57+
pat="your-access-token",
5858
base_url="https://api.qoder.com.cn/api/v1/forward",
5959
timeout=30.0,
6060
max_retries=2,
@@ -358,7 +358,7 @@ For tokens that expire, pass a credential provider instead of a static token. It
358358
client = Forward(credential=my_credential)
359359
```
360360

361-
A static `access_token` takes precedence over a provider, and an explicit `Authorization` header takes precedence over both.
361+
A static `pat` takes precedence over a provider, and an explicit `Authorization` header takes precedence over both.
362362

363363
## Versioning
364364

examples/common/live.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def read_env(path: str | Path) -> dict[str, str]:
5656
@dataclass
5757
class Config:
5858
mode: str
59-
access_token: str = field(repr=False)
59+
pat: str = field(repr=False)
6060
base_url: str = ""
6161
model: str = ""
6262
timeout: float = 300
@@ -89,7 +89,7 @@ def load(cls, mode: str, *, env_file: str = ".env.live", region: str | None = No
8989
return cls(mode, token, str(url), values.get(prefix + "MODEL", ""), timeout)
9090

9191
def client_options(self) -> dict[str, Any]:
92-
return dict(access_token=self.access_token, base_url=self.base_url, max_retries=0, timeout=30)
92+
return dict(pat=self.pat, base_url=self.base_url, max_retries=0, timeout=30)
9393

9494

9595
def safe_error(error: BaseException, token: str = "") -> str:
@@ -163,7 +163,7 @@ def cleanup(self) -> None:
163163
try:
164164
action()
165165
except Exception as error:
166-
failures.append(f"{kind} {resource_id}: {safe_error(error, self.config.access_token)}")
166+
failures.append(f"{kind} {resource_id}: {safe_error(error, self.config.pat)}")
167167
self.cleanups.clear()
168168
if failures:
169169
raise RuntimeError("Cleanup failed:\n" + "\n".join(failures))
@@ -314,9 +314,9 @@ def run_cli(mode: str, client_type: Any, scenarios: dict[str, Callable[[Any, Run
314314
try:
315315
context.cleanup()
316316
except Exception as error:
317-
errors.append(safe_error(error, config.access_token))
317+
errors.append(safe_error(error, config.pat))
318318
except Exception as error:
319-
errors.insert(0, safe_error(error, config.access_token))
319+
errors.insert(0, safe_error(error, config.pat))
320320
results.append({"scenario": scenario, "passed": not errors, "outputs": context.outputs, "errors": errors})
321321
if args.output == "text":
322322
print(f"{scenario}: {'FAIL' if errors else 'PASS'}")

src/qca/common/_base_client.py

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class BaseClient:
5454
def _configure(
5555
self,
5656
*,
57-
access_token: str | None,
57+
pat: str | None,
5858
base_url: str | httpx.URL | None,
5959
timeout: float | httpx.Timeout | None,
6060
max_retries: int,
@@ -64,7 +64,7 @@ def _configure(
6464
) -> None:
6565
if not isinstance(max_retries, int) or isinstance(max_retries, bool) or max_retries < 0:
6666
raise ValueError("max_retries must be a non-negative integer")
67-
self.access_token = access_token if access_token is not None else os.environ.get("QODER_PAT")
67+
self.pat = pat if pat is not None else os.environ.get("QODER_PAT")
6868
self.credential = credential
6969
self.base_url = httpx.URL(base_url or os.environ.get(self._base_url_env) or self._default_base_url)
7070
if self.base_url.scheme not in ("http", "https") or not self.base_url.host or self.base_url.userinfo:
@@ -80,7 +80,7 @@ def _configure(
8080

8181
def _copy(self, *, raw: bool = False, streaming: bool = False, **overrides: Any) -> Self:
8282
options = dict(
83-
access_token=self.access_token,
83+
pat=self.pat,
8484
base_url=self.base_url,
8585
timeout=self.timeout,
8686
max_retries=self.max_retries,
@@ -98,16 +98,14 @@ def _copy(self, *, raw: bool = False, streaming: bool = False, **overrides: Any)
9898
def with_options(
9999
self,
100100
*,
101-
access_token: str | NotGiven = NOT_GIVEN,
101+
pat: str | NotGiven = NOT_GIVEN,
102102
base_url: str | httpx.URL | NotGiven = NOT_GIVEN,
103103
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
104104
max_retries: int | NotGiven = NOT_GIVEN,
105105
default_headers: Mapping[str, str] | NotGiven = NOT_GIVEN,
106106
default_query: Mapping[str, Any] | NotGiven = NOT_GIVEN,
107107
) -> Self:
108-
values: dict[str, Any] = dict(
109-
access_token=access_token, base_url=base_url, timeout=timeout, max_retries=max_retries
110-
)
108+
values: dict[str, Any] = dict(pat=pat, base_url=base_url, timeout=timeout, max_retries=max_retries)
111109
if not isinstance(default_headers, NotGiven):
112110
values["default_headers"] = {**self.default_headers, **default_headers}
113111
if not isinstance(default_query, NotGiven):
@@ -243,7 +241,7 @@ class SyncAPIClient(BaseClient):
243241
def __init__(
244242
self,
245243
*,
246-
access_token: str | None = None,
244+
pat: str | None = None,
247245
base_url: str | httpx.URL | None = None,
248246
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
249247
max_retries: int = 2,
@@ -253,7 +251,7 @@ def __init__(
253251
credential: Credential | None = None,
254252
) -> None:
255253
self._configure(
256-
access_token=access_token,
254+
pat=pat,
257255
base_url=base_url,
258256
timeout=timeout,
259257
max_retries=max_retries,
@@ -270,12 +268,7 @@ def _send(self, request: httpx.Request, *, storage: bool = False) -> httpx.Respo
270268
for attempt in range(self.max_retries + 1):
271269
if track_retries and attempt:
272270
request.headers["X-Qoder-Retry-Count"] = str(attempt)
273-
if (
274-
not storage
275-
and self.credential
276-
and not self.access_token
277-
and "Authorization" not in self.default_headers
278-
):
271+
if not storage and self.credential and not self.pat and "Authorization" not in self.default_headers:
279272
# Dynamic credentials can rotate between retries. Explicit request
280273
# headers still take priority and are marked by request().
281274
if not request.extensions.get("qca_explicit_auth"):
@@ -315,7 +308,7 @@ def request(
315308
download_link: bool = False,
316309
file_fields: list[str] | None = None,
317310
) -> Any:
318-
args = self._request_args(method, path, options, self.access_token, file_fields)
311+
args = self._request_args(method, path, options, self.pat, file_fields)
319312
if stream:
320313
args["headers"]["Accept"] = "text/event-stream"
321314
request = self._client.build_request(**args)
@@ -361,7 +354,7 @@ class AsyncAPIClient(BaseClient):
361354
def __init__(
362355
self,
363356
*,
364-
access_token: str | None = None,
357+
pat: str | None = None,
365358
base_url: str | httpx.URL | None = None,
366359
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
367360
max_retries: int = 2,
@@ -371,7 +364,7 @@ def __init__(
371364
credential: Credential | AsyncCredential | None = None,
372365
) -> None:
373366
self._configure(
374-
access_token=access_token,
367+
pat=pat,
375368
base_url=base_url,
376369
timeout=timeout,
377370
max_retries=max_retries,
@@ -388,12 +381,7 @@ async def _send(self, request: httpx.Request, *, storage: bool = False) -> httpx
388381
for attempt in range(self.max_retries + 1):
389382
if track_retries and attempt:
390383
request.headers["X-Qoder-Retry-Count"] = str(attempt)
391-
if (
392-
not storage
393-
and self.credential
394-
and not self.access_token
395-
and not request.extensions.get("qca_explicit_auth")
396-
):
384+
if not storage and self.credential and not self.pat and not request.extensions.get("qca_explicit_auth"):
397385
token = self.credential.get_token()
398386
if inspect.isawaitable(token):
399387
token = await token
@@ -436,10 +424,10 @@ async def request(
436424
if file_fields:
437425
# Reading local file objects should not block the event loop.
438426
args = await anyio.to_thread.run_sync(
439-
lambda: self._request_args(method, path, options, self.access_token, file_fields)
427+
lambda: self._request_args(method, path, options, self.pat, file_fields)
440428
)
441429
else:
442-
args = self._request_args(method, path, options, self.access_token, None)
430+
args = self._request_args(method, path, options, self.pat, None)
443431
if stream:
444432
args["headers"]["Accept"] = "text/event-stream"
445433
request = self._client.build_request(**args)

tests/_surface.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def _endpoint(mode: str, attribute: str, function: Any) -> Endpoint:
155155
def endpoints() -> list[Endpoint]:
156156
found = []
157157
for mode in ("forward", "managed"):
158-
client = CLIENTS[mode, False](access_token=TOKEN)
158+
client = CLIENTS[mode, False](pat=TOKEN)
159159
found.extend(_endpoint(mode, attribute, function) for attribute, function in _methods(client))
160160
client.close()
161161
return sorted(found, key=lambda endpoint: endpoint.id)
@@ -197,7 +197,7 @@ def client_for(endpoint: Endpoint, async_: bool, handle: Callable[[httpx.Request
197197
transport = httpx.MockTransport(handle)
198198
http_client = httpx.AsyncClient(transport=transport) if async_ else httpx.Client(transport=transport)
199199
return CLIENTS[endpoint.mode, async_](
200-
access_token=TOKEN,
200+
pat=TOKEN,
201201
base_url=f"{BASE_URL}/{endpoint.mode}",
202202
max_retries=0,
203203
http_client=http_client,
@@ -207,7 +207,7 @@ def client_for(endpoint: Endpoint, async_: bool, handle: Callable[[httpx.Request
207207

208208
def plain_client(mode: str, async_: bool) -> Any:
209209
"""A client for tests that only introspect the resource tree."""
210-
return CLIENTS[mode, async_](access_token=TOKEN)
210+
return CLIENTS[mode, async_](pat=TOKEN)
211211

212212

213213
async def call(endpoint: Endpoint, client: Any, *, raw: bool = False, **overrides: Any) -> Any:

tests/test_client.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
def make_client(handler, cls=Forward, **kwargs):
3737
return cls(
38-
access_token="test-token",
38+
pat="test-token",
3939
base_url="https://api.test/prefix/",
4040
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
4141
**kwargs,
@@ -49,10 +49,10 @@ def test_environment_defaults_and_explicit_precedence(monkeypatch, cls, env, suf
4949
monkeypatch.setenv("QODER_PAT", "from-env")
5050
monkeypatch.setenv(env, f"https://configured.test/api/v1/{suffix}")
5151
with cls() as client:
52-
assert client.access_token == "from-env"
52+
assert client.pat == "from-env"
5353
assert str(client.base_url) == f"https://configured.test/api/v1/{suffix}/"
54-
with cls(access_token="explicit", base_url="https://explicit.test/root") as client:
55-
assert client.access_token == "explicit"
54+
with cls(pat="explicit", base_url="https://explicit.test/root") as client:
55+
assert client.pat == "explicit"
5656
assert str(client.base_url) == "https://explicit.test/root/"
5757

5858

@@ -383,7 +383,7 @@ def handle(request):
383383
http = httpx.Client(
384384
transport=httpx.MockTransport(handle), headers={"x-sensitive": "secret"}, cookies={"session": "cookie"}
385385
)
386-
with Managed(access_token="api-token", base_url="https://api.test/cloud", http_client=http) as client:
386+
with Managed(pat="api-token", base_url="https://api.test/cloud", http_client=http) as client:
387387
response = client.files.download("file", workspace_id="workspace")
388388
response.write_to_file(tmp_path / "download.txt")
389389
assert response.http_response.is_closed
@@ -439,7 +439,7 @@ async def handle(request):
439439
return httpx.Response(200, json={"url": "https://storage.test/asset"})
440440

441441
async with AsyncManaged(
442-
access_token="secret", http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle))
442+
pat="secret", http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle))
443443
) as client:
444444
item = await client.files.upload(file=("file.txt", b"hello"))
445445
result = await client.files.download(item.id)
@@ -533,7 +533,7 @@ def handle(request):
533533
calls.append(request)
534534
return httpx.Response(status, json={"error": {"message": "retry"}}, headers={"retry-after-ms": "25"})
535535

536-
async with cls(access_token="test", http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle))) as client:
536+
async with cls(pat="test", http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle))) as client:
537537
with pytest.raises(APIStatusError):
538538
if verb == "GET":
539539
await client.models.list()
@@ -558,7 +558,7 @@ def handle(request):
558558
return httpx.Response(500, json={})
559559

560560
transport = httpx.MockTransport(handle)
561-
async with AsyncForward(access_token="test", http_client=httpx.AsyncClient(transport=transport)) as client:
561+
async with AsyncForward(pat="test", http_client=httpx.AsyncClient(transport=transport)) as client:
562562
with pytest.raises(APIStatusError):
563563
await client.models.list()
564564
assert counts == ["0", "1", "2"]

tests/test_examples.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -327,10 +327,10 @@ def test_config_file_is_data_and_environment_wins(tmp_path, monkeypatch):
327327
monkeypatch.setenv("QODER_FORWARD_PAT", "env-token")
328328
monkeypatch.delenv("QODER_FORWARD_BASE_URL", raising=False)
329329
config = Config.load("forward", env_file=str(path))
330-
assert config.access_token == "env-token"
330+
assert config.pat == "env-token"
331331
assert config.model == "ultimate"
332332
assert "env-token" not in repr(config)
333-
assert "env-token" not in safe_error(ValueError("env-token"), config.access_token)
333+
assert "env-token" not in safe_error(ValueError("env-token"), config.pat)
334334

335335

336336
def test_choose_model_rejects_disabled_preference():
@@ -350,7 +350,7 @@ def test_single_scenario_cli_defaults_to_that_scenario_and_cleans_up(fails, monk
350350
monkeypatch.setattr(Config, "load", lambda *args, **kwargs: config)
351351
monkeypatch.setattr(sys, "argv", ["examples.forward.memory", "--output", "json"])
352352
http_client = httpx.Client(transport=httpx.MockTransport(lambda request: httpx.Response(200)))
353-
client = Forward(access_token=config.access_token, http_client=http_client)
353+
client = Forward(pat=config.pat, http_client=http_client)
354354
calls = []
355355

356356
def memory(client, context):
@@ -476,7 +476,7 @@ def test_status_error_shows_server_reason_and_redacts_token_and_signed_url():
476476
from qca import BadRequestError
477477

478478
with Forward(
479-
access_token="test-token",
479+
pat="test-token",
480480
base_url="https://api.test/api/v1/forward",
481481
http_client=httpx.Client(
482482
transport=httpx.MockTransport(

tests/test_resource_surface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async def test_resource_tree_is_built_once_and_reused(mode, async_):
175175

176176

177177
def test_unknown_resource_attributes_fail_loudly():
178-
with Forward(access_token=TOKEN, http_client=httpx.Client()) as client:
178+
with Forward(pat=TOKEN, http_client=httpx.Client()) as client:
179179
with pytest.raises(AttributeError):
180180
client.sessions.no_such_method
181181
with pytest.raises(AttributeError):

0 commit comments

Comments
 (0)