diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9c2cb..a95c156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [v0.2.0] - 2026-07-02 + +### Added + +- Added a `request_timeout_seconds` constructor parameter to `PipelexAPIClient`, setting a per-instance blocking-execute ceiling for the inherited protocol routes (`execute`, `start`, `validate`, `models`, `version`). + +### Changed + +- **BREAKING:** Renamed `PipelexAPIClient` constructor parameters and attributes to match the `mthds` base client and the `@pipelex/sdk` JavaScript counterpart: `api_token` → `api_key` and `api_base_url` → `base_url`. *(Migration: update all instantiations and property reads to the new names.)* +- **BREAKING:** Renamed API URL environment variables for workspace-wide consistency: `PIPELEX_API_URL` → `PIPELEX_BASE_URL` and the `mthds` fallback `MTHDS_API_URL` → `MTHDS_BASE_URL`. No read aliases are kept for the old names. +- Bumped the `mthds` dependency from `>=0.6.1` to `>=0.7.0`, and updated config loading to import `load_config` from `mthds.config` (previously `load_credentials` from `mthds.config.credentials`). +- Updated documentation (`README.md`, `CLAUDE.md`, `docs/architecture.md`) and unit tests to reflect the new client signature, environment variables, and `mthds` configuration. + ## [v0.1.1] - 2026-07-01 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 4345a6e..2866c79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ It is the **hosted superset**: the five normative MTHDS Protocol routes (inherit - **One-way dependency: `pipelex-sdk → mthds`.** This package depends on `mthds` and never the reverse. - **Inheritance, not re-implementation.** `class PipelexAPIClient(MthdsAPIClient)`. Reuse the base transport (`_send`, `_url`), body-builders, the reusable protocol methods, `runner_type`, and the async context-manager. Add lifecycle/product/health on top. The base's single-underscore transport methods are treated as a documented **protected extension surface** — do not rename or fork them. - **Brand boundary (MTHDS vs Pipelex).** MTHDS = the open standard's brand; Pipelex = the runtime/product brand. Protocol routes and their models belong to `mthds` and keep neutral names; Pipelex-specific surfaces (lifecycle, product routes, implementation envelopes) live here. Name by which brand owns the concept. -- **Credentials.** Resolve `PIPELEX_API_KEY` / `PIPELEX_API_URL` first, then fall back to the `mthds` resolver (`MTHDS_API_KEY` / `MTHDS_API_URL`, `~/.mthds/config`). Token is **optional** (anonymous allowed). Default base URL `https://api.pipelex.com`. +- **Credentials.** Resolve `PIPELEX_API_KEY` / `PIPELEX_BASE_URL` first, then fall back to the `mthds` resolver (`MTHDS_API_KEY` / `MTHDS_BASE_URL`, `~/.mthds/config`). Token is **optional** (anonymous allowed). Default base URL `https://api.pipelex.com`. - **Async-only.** httpx `AsyncClient`, `async def` throughout. No sync facade in v0.1. - **No barrel.** `__init__.py` files stay empty — no re-exports, no docstrings. Import via full paths (`from pipelex_sdk.client import PipelexAPIClient`). diff --git a/README.md b/README.md index fcae8b2..3c3411c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ pip install pipelex-sdk ## Configuration -Credentials resolve, in order: explicit constructor arguments → `PIPELEX_API_KEY` / `PIPELEX_API_URL` → `MTHDS_API_KEY` / `MTHDS_API_URL` (and `~/.mthds/config`) → defaults. The token is **optional** — anonymous access works against the protocol routes (e.g. a local bare runner); the product routes return `401`. The default base URL is `https://api.pipelex.com`. The base URL is host-only (no path/query/fragment); every endpoint composes as `{base}/v1/{endpoint}`. +Credentials resolve, in order: explicit constructor arguments → `PIPELEX_API_KEY` / `PIPELEX_BASE_URL` → `MTHDS_API_KEY` / `MTHDS_BASE_URL` (and `~/.mthds/config`) → defaults. The token is **optional** — anonymous access works against the protocol routes (e.g. a local bare runner); the product routes return `401`. The default base URL is `https://api.pipelex.com`. The base URL is host-only (no path/query/fragment); every endpoint composes as `{base}/v1/{endpoint}`. The client is **async-only** (httpx `AsyncClient`) and is an async context manager. diff --git a/docs/architecture.md b/docs/architecture.md index 52c94cb..65a71ce 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -35,8 +35,8 @@ The Pipelex narrowing of the `/v1/validate` verdict union is one such implementa Resolved at construction time: -- `PIPELEX_API_KEY` / `PIPELEX_API_URL` first (brand + JS parity); -- falling back to the `mthds` resolver (`MTHDS_API_KEY` / `MTHDS_API_URL`, `~/.mthds/config`) as a secondary source. +- `PIPELEX_API_KEY` / `PIPELEX_BASE_URL` first (brand + JS parity); +- falling back to the `mthds` resolver (`MTHDS_API_KEY` / `MTHDS_BASE_URL`, `~/.mthds/config`) as a secondary source. A token is **optional** (anonymous access is allowed; protocol routes work against anonymous bare runners, product routes return `401`). The default base URL is `https://api.pipelex.com`. The base URL is validated host-only (no path/query/fragment/embedded credentials; http/https only). diff --git a/pipelex_sdk/client.py b/pipelex_sdk/client.py index 46d51e2..963d802 100644 --- a/pipelex_sdk/client.py +++ b/pipelex_sdk/client.py @@ -26,7 +26,7 @@ from urllib.parse import quote, urlparse import httpx -from mthds.config.credentials import load_credentials +from mthds.config import load_config from mthds.protocol.exceptions import PipelineRequestError from mthds.runners.api.client import MthdsAPIClient from pydantic import BaseModel, TypeAdapter, ValidationError @@ -89,7 +89,7 @@ from pipelex_sdk.runs import RunResultState from pipelex_sdk.validation_models import PipelexValidationResult -# The client composes every endpoint from one origin (PIPELEX_API_URL): `{base}/v1/{endpoint}`. +# The client composes every endpoint from one origin (PIPELEX_BASE_URL): `{base}/v1/{endpoint}`. # The same paths are served by the Pipelex Hosted API (api.pipelex.com) and by a bare # OSS pipelex-api runner (localhost:8081) — the protocol surface is identical; only the # hosted extensions (e.g. run polling) differ, detectable via GET /v1/version. @@ -109,7 +109,7 @@ _GATEWAY_TIMEOUT_THRESHOLD_SECONDS = 28.0 _PIPELEX_API_KEY_ENV = "PIPELEX_API_KEY" -_PIPELEX_API_URL_ENV = "PIPELEX_API_URL" +_PIPELEX_BASE_URL_ENV = "PIPELEX_BASE_URL" # `VersionInfo.implementation` of the bare open-source runner (no run store). Anything # else — the hosted implementation first — is assumed to serve the durable run-lifecycle @@ -139,7 +139,7 @@ class MthdsFile(BaseModel): class PipelexAPIClient(MthdsAPIClient): """Client for the Pipelex hosted API — and any MTHDS-compliant runner. - One base URL (`PIPELEX_API_URL`); every endpoint is `/v1/`: + One base URL (`PIPELEX_BASE_URL`); every endpoint is `/v1/`: - **protocol** (`execute` / `start` / `validate` / `models` / `version`) — inherited from `MthdsAPIClient`; works against any MTHDS-compliant runner, hosted or bare. - **run lifecycle** (`get_run_status` / `get_run_result` / `wait_for_result`) — the @@ -149,30 +149,36 @@ class PipelexAPIClient(MthdsAPIClient): on the structured `ApiResponseError.code`, not the HTTP status. Construction resolves credentials Pipelex-first (`PIPELEX_API_KEY` / - `PIPELEX_API_URL`), falling back to the `mthds` resolver (`MTHDS_API_KEY` / - `MTHDS_API_URL`, `~/.mthds/config`). The token is optional — anonymous access works + `PIPELEX_BASE_URL`), falling back to the `mthds` resolver (`MTHDS_API_KEY` / + `MTHDS_BASE_URL`, `~/.mthds/config`). The token is optional — anonymous access works against the protocol routes; product routes return `401`. The base URL is validated host-only (no path/query/fragment/credentials; http/https only). """ - def __init__(self, api_token: str | None = None, api_base_url: str | None = None) -> None: - credentials = load_credentials() + def __init__(self, api_key: str | None = None, base_url: str | None = None) -> None: + config = load_config() - # Pipelex-primary, mthds fallback. `credentials` already layers env (MTHDS_*) > + # Pipelex-primary, mthds fallback. `config` already layers env (MTHDS_*) > # file (~/.mthds/config) > default, so this ladder gives the full precedence: - # explicit arg > PIPELEX_* env > MTHDS_* env > file > default. The token is optional + # explicit arg > PIPELEX_* env > MTHDS_* env > file > default. The key is optional # and an empty string ("") means anonymous — so the first layer that is *present* - # wins even when it is empty. We test `is not None` (not truthiness) to honor an - # explicit `api_token=""` / `PIPELEX_API_KEY=""`, matching the JS SDK's `??` chain. - self.api_token: str - if api_token is not None: - self.api_token = api_token - elif (pipelex_env_token := os.environ.get(_PIPELEX_API_KEY_ENV)) is not None: - self.api_token = pipelex_env_token + # wins even when it is empty. + # + # DO NOT collapse this into `api_key or os.environ.get(...) or config["api_key"]`. + # `or` treats "" as falsy and would fall through, silently discarding an explicit + # anonymous request (`api_key=""` / `PIPELEX_API_KEY=""`) and reaching for the next + # configured key instead. We test `is not None` (presence, not truthiness) precisely + # to honor the empty string, matching the JS SDK's `??` chain. (The `base_url` line + # below correctly uses `or`: there an empty value has no special meaning.) + self.api_key: str + if api_key is not None: + self.api_key = api_key + elif (pipelex_env_key := os.environ.get(_PIPELEX_API_KEY_ENV)) is not None: + self.api_key = pipelex_env_key else: - self.api_token = credentials["api_key"] + self.api_key = config["api_key"] - resolved_base_url = api_base_url or os.environ.get(_PIPELEX_API_URL_ENV) or credentials["api_url"] or DEFAULT_API_BASE_URL + resolved_base_url = base_url or os.environ.get(_PIPELEX_BASE_URL_ENV) or config["base_url"] or DEFAULT_API_BASE_URL normalized_base_url = resolved_base_url.rstrip("/") # The base URL must be host-only: a path-prefixed value (e.g. `.../v1`) would # compose as `/v1/v1/...` and fail with a misleading endpoint error instead of a @@ -185,19 +191,23 @@ def __init__(self, api_token: str | None = None, api_base_url: str | None = None "Endpoints compose as {base}/v1/{endpoint}." ) raise PipelineRequestError(msg) - self.api_base_url: str = normalized_base_url + self.base_url: str = normalized_base_url #: Origin root derived from the base URL — `/health` lives here, not under `/v1`. self.origin_url: str = _origin_of(normalized_base_url) + #: Per-request timeout the inherited protocol routes (`execute` / `start` / `validate` + #: / `models` / `version`) read — the blocking-execute ceiling. The SDK's own poll and + #: product GETs pass `_POLL_REQUEST_TIMEOUT_SECONDS` explicitly instead. + self.request_timeout_seconds: float = _DEFAULT_REQUEST_TIMEOUT_SECONDS self.client: httpx.AsyncClient | None = None #: Cached `/v1/version` handshake outcome — whether the durable lifecycle is served. self._lifecycle_available: bool | None = None @override def start_client(self) -> PipelexAPIClient: - """Initialize the HTTP client. The Authorization header is sent only when a token - is configured — anonymous access (empty token) omits it, matching the JS SDK. + """Initialize the HTTP client. The Authorization header is sent only when a key + is configured — anonymous access (empty key) omits it, matching the JS SDK. """ - headers = {"Authorization": f"Bearer {self.api_token}"} if self.api_token else {} + headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {} self.client = httpx.AsyncClient(headers=headers) return self @@ -211,12 +221,12 @@ async def _send_or_unreachable(self, method: str, url: str, *, content: bytes | try: return await self._send(method, url, content=content, request_timeout=request_timeout) except httpx.TimeoutException as exc: - msg = f"Could not reach Pipelex API at {self.api_base_url} (timeout)" - raise ApiUnreachableError(msg, api_url=self.api_base_url, code="ABORT_TIMEOUT") from exc + msg = f"Could not reach Pipelex API at {self.base_url} (timeout)" + raise ApiUnreachableError(msg, api_url=self.base_url, code="ABORT_TIMEOUT") from exc except httpx.TransportError as exc: code = type(exc).__name__ - msg = f"Could not reach Pipelex API at {self.api_base_url} ({code})" - raise ApiUnreachableError(msg, api_url=self.api_base_url, code=code) from exc + msg = f"Could not reach Pipelex API at {self.base_url} ({code})" + raise ApiUnreachableError(msg, api_url=self.base_url, code=code) from exc async def _request_product(self, method: str, endpoint: str, *, body: object | None = None) -> Any: """Issue a Pipelex-product request (`/v1/me`, `/v1/methods`, `/v1/billing/*`, …) @@ -256,7 +266,7 @@ def _raise_api_response_error(self, *, method: str, endpoint: str, response: htt msg = f"API {method} /{_API_PREFIX}/{endpoint} failed ({response.status_code}): {detail}" raise ApiResponseError( msg, - api_url=self.api_base_url, + api_url=self.base_url, status=response.status_code, status_text=response.reason_phrase, response_body=body_text, @@ -277,9 +287,9 @@ def _raise_if_lifecycle_unavailable(self, response: httpx.Response, url: str) -> msg = ( f"The durable run lifecycle is not available: {url} returned 404. Run polling is a " f"hosted-API extension (/{_API_PREFIX}/{_RUNS}/*), not part of the MTHDS Protocol; " - "PIPELEX_API_URL points at a bare runner that does not serve it." + "PIPELEX_BASE_URL points at a bare runner that does not serve it." ) - raise RunLifecycleUnavailableError(msg, api_url=self.api_base_url) + raise RunLifecycleUnavailableError(msg, api_url=self.base_url) # ── Protocol surface: `execute` override (gateway-timeout translation) ── diff --git a/pipelex_sdk/errors.py b/pipelex_sdk/errors.py index b7de7c5..05a5cc2 100644 --- a/pipelex_sdk/errors.py +++ b/pipelex_sdk/errors.py @@ -133,7 +133,7 @@ def __init__(self, message: str, run_id: str, timeout_seconds: float) -> None: class RunLifecycleUnavailableError(PipelineRequestError): """Raised when the durable run lifecycle (`/v1/runs/*`) is not served by the - configured `PIPELEX_API_URL`. + configured `PIPELEX_BASE_URL`. Run polling is a hosted-API extension, not part of the MTHDS Protocol: the open-source `pipelex-api` runner executes methods but has no run store, so it diff --git a/pyproject.toml b/pyproject.toml index a39fa9f..5654006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pipelex-sdk" -version = "0.1.1" +version = "0.2.0" description = "The Python client for the Pipelex hosted API — the MTHDS Protocol surface plus the durable run lifecycle and the Pipelex product surface, built on the `mthds` protocol base." authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }] maintainers = [{ name = "Pipelex staff", email = "oss@pipelex.com" }] @@ -19,7 +19,7 @@ classifiers = [ ] dependencies = [ - "mthds>=0.6.1", + "mthds>=0.7.0", "pydantic>=2.10.6,<3.0.0", "backports.strenum>=1.3.0 ; python_version < '3.11'", "typing-extensions>=4.0.0", diff --git a/tests/unit/test_client_construction.py b/tests/unit/test_client_construction.py index d51adaa..2fad2c2 100644 --- a/tests/unit/test_client_construction.py +++ b/tests/unit/test_client_construction.py @@ -8,7 +8,7 @@ from pipelex_sdk.client import PipelexAPIClient -_MTHDS_DEFAULT_CREDENTIALS = {"api_key": "", "api_url": "https://api.pipelex.com", "runner": "api", "telemetry": "0"} +_MTHDS_DEFAULT_CREDENTIALS = {"api_key": "", "base_url": "https://api.pipelex.com", "runner": "api"} class TestClientConstruction: @@ -16,61 +16,61 @@ class TestClientConstruction: def _isolate_env(self, mocker: MockerFixture) -> None: """Hermetic construction — no real env vars, mthds resolver returns defaults.""" mocker.patch.dict(os.environ, {}, clear=True) - mocker.patch("pipelex_sdk.client.load_credentials", return_value=dict(_MTHDS_DEFAULT_CREDENTIALS)) + mocker.patch("pipelex_sdk.client.load_config", return_value=dict(_MTHDS_DEFAULT_CREDENTIALS)) def test_defaults_to_hosted_base_and_anonymous(self) -> None: client = PipelexAPIClient() - assert client.api_base_url == "https://api.pipelex.com" + assert client.base_url == "https://api.pipelex.com" assert client.origin_url == "https://api.pipelex.com" - assert client.api_token == "" + assert client.api_key == "" def test_pipelex_env_takes_precedence_over_mthds(self, mocker: MockerFixture) -> None: - mocker.patch.dict(os.environ, {"PIPELEX_API_KEY": "pk-live", "PIPELEX_API_URL": "http://localhost:8081"}, clear=True) + mocker.patch.dict(os.environ, {"PIPELEX_API_KEY": "pk-live", "PIPELEX_BASE_URL": "http://localhost:8081"}, clear=True) mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "mthds-key", "api_url": "https://mthds.example.com", "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "mthds-key", "base_url": "https://mthds.example.com", "runner": "api"}, ) client = PipelexAPIClient() - assert client.api_token == "pk-live" - assert client.api_base_url == "http://localhost:8081" + assert client.api_key == "pk-live" + assert client.base_url == "http://localhost:8081" def test_falls_back_to_mthds_credentials(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "mthds-key", "api_url": "https://mthds.example.com", "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "mthds-key", "base_url": "https://mthds.example.com", "runner": "api"}, ) client = PipelexAPIClient() - assert client.api_token == "mthds-key" - assert client.api_base_url == "https://mthds.example.com" + assert client.api_key == "mthds-key" + assert client.base_url == "https://mthds.example.com" def test_explicit_args_override_env_and_credentials(self, mocker: MockerFixture) -> None: - mocker.patch.dict(os.environ, {"PIPELEX_API_KEY": "pk-env", "PIPELEX_API_URL": "http://env.example.com"}, clear=True) - client = PipelexAPIClient(api_token="arg-token", api_base_url="https://arg.example.com") - assert client.api_token == "arg-token" - assert client.api_base_url == "https://arg.example.com" + mocker.patch.dict(os.environ, {"PIPELEX_API_KEY": "pk-env", "PIPELEX_BASE_URL": "http://env.example.com"}, clear=True) + client = PipelexAPIClient(api_key="arg-token", base_url="https://arg.example.com") + assert client.api_key == "arg-token" + assert client.base_url == "https://arg.example.com" def test_explicit_empty_token_forces_anonymous_over_env(self, mocker: MockerFixture) -> None: - """An explicit `api_token=""` means anonymous and must win over a configured env token.""" + """An explicit `api_key=""` means anonymous and must win over a configured env token.""" mocker.patch.dict(os.environ, {"PIPELEX_API_KEY": "pk-env"}, clear=True) - client = PipelexAPIClient(api_token="") - assert client.api_token == "" + client = PipelexAPIClient(api_key="") + assert client.api_key == "" def test_explicit_empty_token_forces_anonymous_over_credentials(self, mocker: MockerFixture) -> None: - """An explicit `api_token=""` must win over an mthds credential token too.""" + """An explicit `api_key=""` must win over an mthds credential token too.""" mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "mthds-key", "api_url": "https://mthds.example.com", "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "mthds-key", "base_url": "https://mthds.example.com", "runner": "api"}, ) - client = PipelexAPIClient(api_token="") - assert client.api_token == "" + client = PipelexAPIClient(api_key="") + assert client.api_key == "" def test_strips_trailing_slash(self) -> None: - client = PipelexAPIClient(api_base_url="https://api.pipelex.com/") - assert client.api_base_url == "https://api.pipelex.com" + client = PipelexAPIClient(base_url="https://api.pipelex.com/") + assert client.base_url == "https://api.pipelex.com" assert client.origin_url == "https://api.pipelex.com" def test_origin_includes_port(self) -> None: - client = PipelexAPIClient(api_base_url="http://localhost:8081") + client = PipelexAPIClient(base_url="http://localhost:8081") assert client.origin_url == "http://localhost:8081" @pytest.mark.parametrize( @@ -87,4 +87,4 @@ def test_origin_includes_port(self) -> None: ) def test_rejects_non_host_only_base_url(self, bad_url: str) -> None: with pytest.raises(PipelineRequestError): - PipelexAPIClient(api_base_url=bad_url) + PipelexAPIClient(base_url=bad_url) diff --git a/tests/unit/test_client_execute.py b/tests/unit/test_client_execute.py index 75d3e07..6e99adf 100644 --- a/tests/unit/test_client_execute.py +++ b/tests/unit/test_client_execute.py @@ -34,12 +34,12 @@ class TestClientExecute: @pytest.fixture(autouse=True) def _mock_credentials(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": "", "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": "", "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="test-token", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="test-token", base_url=_BASE_URL) def test_gateway_503_past_ceiling_translates_to_timeout(self, mocker: MockerFixture) -> None: client = self._client() diff --git a/tests/unit/test_client_health.py b/tests/unit/test_client_health.py index b9213da..23906b0 100644 --- a/tests/unit/test_client_health.py +++ b/tests/unit/test_client_health.py @@ -27,12 +27,12 @@ class TestClientHealth: @pytest.fixture(autouse=True) def _isolate(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": _BASE_URL, "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": _BASE_URL, "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="t", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="t", base_url=_BASE_URL) def test_health_hits_origin_level_path_outside_v1(self, mocker: MockerFixture) -> None: client = self._client() diff --git a/tests/unit/test_client_lifecycle.py b/tests/unit/test_client_lifecycle.py index 346915f..39280be 100644 --- a/tests/unit/test_client_lifecycle.py +++ b/tests/unit/test_client_lifecycle.py @@ -40,23 +40,23 @@ class TestClientLifecycle: def _mock_credentials(self, mocker: MockerFixture) -> None: """Keep construction hermetic — never touch the real credentials file/env.""" mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": "", "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": "", "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="test-token", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="test-token", base_url=_BASE_URL) # ── start (inherited body-building + bare-runner 404 translation) ── def test_start_targets_v1_url_and_returns_run_result_start(self, mocker: MockerFixture) -> None: """Start posts to /v1/start; a 202 parses into RunResultStart with the authoritative id.""" - client = PipelexAPIClient(api_token="t", api_base_url=f"{_BASE_URL}/") + client = PipelexAPIClient(api_key="t", base_url=f"{_BASE_URL}/") body = {"pipeline_run_id": "run_1", "state": "RUNNING", "created_at": "2026-06-10T00:00:00Z"} send_mock = mocker.patch.object(client, "_send", mocker.AsyncMock(return_value=_response(202, json=body))) started = asyncio.run(client.start(pipe_code="answer")) - assert client.api_base_url == _BASE_URL + assert client.base_url == _BASE_URL assert send_mock.call_args.args[1] == f"{_BASE_URL}/v1/start" assert started.pipeline_run_id == "run_1" diff --git a/tests/unit/test_client_product.py b/tests/unit/test_client_product.py index 57a2356..ea98eeb 100644 --- a/tests/unit/test_client_product.py +++ b/tests/unit/test_client_product.py @@ -51,12 +51,12 @@ class TestClientProduct: @pytest.fixture(autouse=True) def _isolate(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": _BASE_URL, "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": _BASE_URL, "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="test-token", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="test-token", base_url=_BASE_URL) def _mock_send(self, mocker: MockerFixture, client: PipelexAPIClient, response: httpx.Response) -> MockType: return mocker.patch.object(client, "_send", mocker.AsyncMock(return_value=response)) diff --git a/tests/unit/test_client_run_fallback.py b/tests/unit/test_client_run_fallback.py index 3fb14f9..2072f73 100644 --- a/tests/unit/test_client_run_fallback.py +++ b/tests/unit/test_client_run_fallback.py @@ -45,12 +45,12 @@ class TestClientRunFallback: @pytest.fixture(autouse=True) def _mock_credentials(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": "", "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": "", "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="test-token", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="test-token", base_url=_BASE_URL) # ── Hosted (durable start + poll) ──────────────────────────── diff --git a/tests/unit/test_client_transport.py b/tests/unit/test_client_transport.py index fc6ec60..041566a 100644 --- a/tests/unit/test_client_transport.py +++ b/tests/unit/test_client_transport.py @@ -27,12 +27,12 @@ class TestClientTransport: @pytest.fixture(autouse=True) def _isolate(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": _BASE_URL, "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": _BASE_URL, "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="t", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="t", base_url=_BASE_URL) # ── _request_product ───────────────────────────────────────────── diff --git a/tests/unit/test_client_validate.py b/tests/unit/test_client_validate.py index 6795a22..d0840c8 100644 --- a/tests/unit/test_client_validate.py +++ b/tests/unit/test_client_validate.py @@ -28,12 +28,12 @@ class TestClientValidate: @pytest.fixture(autouse=True) def _isolate(self, mocker: MockerFixture) -> None: mocker.patch( - "pipelex_sdk.client.load_credentials", - return_value={"api_key": "", "api_url": _BASE_URL, "runner": "api", "telemetry": "0"}, + "pipelex_sdk.client.load_config", + return_value={"api_key": "", "base_url": _BASE_URL, "runner": "api"}, ) def _client(self) -> PipelexAPIClient: - return PipelexAPIClient(api_token="t", api_base_url=_BASE_URL) + return PipelexAPIClient(api_key="t", base_url=_BASE_URL) def _mock_send(self, mocker: MockerFixture, client: PipelexAPIClient, *, json_body: object) -> MockType: response = httpx.Response(200, json=json_body, request=httpx.Request("POST", f"{_BASE_URL}/v1/validate")) diff --git a/uv.lock b/uv.lock index 29599cc..9bb2138 100644 --- a/uv.lock +++ b/uv.lock @@ -250,7 +250,7 @@ wheels = [ [[package]] name = "mthds" -version = "0.6.1" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-strenum", marker = "python_full_version < '3.11'" }, @@ -261,9 +261,9 @@ dependencies = [ { name = "tomlkit" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/33/c274de1115b6cbe8ac3475327a622893070f1ca40d9f2d0172d6fcae62b9/mthds-0.6.1.tar.gz", hash = "sha256:3d6b93306708f0ef8971285cfab4d9a488fca364e5db7e63a7dba2d065070eef", size = 132504, upload-time = "2026-07-01T07:09:50.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/fe/52bf1818f41b71658f24ee78dfe24730c04c1066e8b434f39fe23823cbb4/mthds-0.7.0.tar.gz", hash = "sha256:35e8c9d5f4b27e86fec02245f72bc2eb9c849790d39a8ffe6dcb001e7fe7d4cb", size = 136385, upload-time = "2026-07-02T20:17:58.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/52/0538d1bd11067f60f86ad74dc4d36bcad81ff3b549f71b3290aebd3a9aeb/mthds-0.6.1-py3-none-any.whl", hash = "sha256:5cd39d2b6fc5b43c5967162d80178b050748502515eda26916c2b80e8f3f1f42", size = 57705, upload-time = "2026-07-01T07:09:49.256Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/f12c6de8a2ed9938a5914559639921ded8275f42b8c43ac1af35fe7a488f/mthds-0.7.0-py3-none-any.whl", hash = "sha256:4e371b73ba259899f5ba0b460809d00f176ac44ad773073a3ebb6c10ee207866", size = 57898, upload-time = "2026-07-02T20:17:57.525Z" }, ] [[package]] @@ -350,7 +350,7 @@ wheels = [ [[package]] name = "pipelex-sdk" -version = "0.1.1" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "backports-strenum", marker = "python_full_version < '3.11'" }, @@ -375,7 +375,7 @@ dev = [ requires-dist = [ { name = "backports-strenum", marker = "python_full_version < '3.11'", specifier = ">=1.3.0" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, - { name = "mthds", specifier = ">=0.6.1" }, + { name = "mthds", specifier = ">=0.7.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.19.1" }, { name = "pydantic", specifier = ">=2.10.6,<3.0.0" }, { name = "pylint", marker = "extra == 'dev'", specifier = "==4.0.4" },