From 8fd6db579ad0e7d82820414ce2fd76efc18716e7 Mon Sep 17 00:00:00 2001 From: Valeriia Date: Thu, 11 Jun 2026 11:29:32 +0200 Subject: [PATCH 1/5] fix(pagination): align termination contract with Go SDK --- src/sdk/core/pagination.py | 15 +++----- tests/unit/core/test_pagination.py | 57 ++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/sdk/core/pagination.py b/src/sdk/core/pagination.py index c65026e..803cecb 100644 --- a/src/sdk/core/pagination.py +++ b/src/sdk/core/pagination.py @@ -53,8 +53,8 @@ def marker_paginate( """Paginate using marker-based strategy. Fetches pages by setting ``marker`` query param to the last - item's ``marker_key`` value. Stops when a page returns - fewer items than ``limit`` or an empty list. + item's ``marker_key`` value. Stops on an empty page, a missing/empty marker on the last item, + or a repeated marker. Args: client: Service client to send requests through. @@ -86,8 +86,6 @@ def marker_paginate( for item in items: yield model.model_validate(item) if model else item - if limit and len(items) < limit: - return last = items[-1] raw_marker = last.get(marker_key) @@ -95,8 +93,8 @@ def marker_paginate( return marker_str = str(raw_marker) - if query.get("marker") == marker_str: - return + # if query.get("marker") == marker_str: + # return query["marker"] = marker_str @@ -149,10 +147,7 @@ def offset_paginate( for item in items: yield model.model_validate(item) if model else item - if len(items) < limit: - return - - offset += limit + offset += len(items) def linked_paginate( diff --git a/tests/unit/core/test_pagination.py b/tests/unit/core/test_pagination.py index b9839f1..a2d34de 100644 --- a/tests/unit/core/test_pagination.py +++ b/tests/unit/core/test_pagination.py @@ -107,7 +107,10 @@ def test_empty_path(self) -> None: class TestMarkerPaginate: def test_single_page(self) -> None: """Single page with fewer items than limit.""" + def handler(req: httpx.Request) -> httpx.Response: + if "marker=" in str(req.url): + return httpx.Response(200, json={"servers": []}) return httpx.Response(200, json={ "servers": [ {"id": "s1", "name": "a"}, @@ -133,16 +136,17 @@ def handler(req: httpx.Request) -> httpx.Response: return httpx.Response(200, json={ "items": [{"id": "1"}, {"id": "2"}], }) - else: + elif "marker=2" in url: return httpx.Response(200, json={ "items": [{"id": "3"}], }) + return httpx.Response(200, json={"items": []}) sc = _make_service_client(handler) items = list(marker_paginate(sc, "items", items_key="items", limit=2)) assert len(items) == 3 - assert call_count == 2 + assert call_count == 3 assert items[-1]["id"] == "3" def test_empty_first_page(self) -> None: @@ -226,6 +230,28 @@ def handler(req: httpx.Request) -> httpx.Response: assert "status=ACTIVE" in str(captured[0].url) + def test_server_caps_page_size(self) -> None: + """Server caps pages at 2 even though we asked limit=100 — + Go-parity pagination must traverse everything (stops on empty + page only), not at the first short page.""" + ids = ["a", "b", "c", "d", "e"] + + def handler(req: httpx.Request) -> httpx.Response: + url = str(req.url) + tail = ids + for i in ids: + if f"marker={i}" in url: + tail = ids[ids.index(i) + 1:] + break + return httpx.Response(200, json={ + "items": [{"id": x} for x in tail[:2]], # cap wins over limit + }) + + sc = _make_service_client(handler) + items = list(marker_paginate(sc, "items", items_key="items", limit=100)) + + assert [i["id"] for i in items] == ids + # ====================================================================== # offset_paginate @@ -235,9 +261,11 @@ def handler(req: httpx.Request) -> httpx.Response: class TestOffsetPaginate: def test_single_page(self) -> None: def handler(req: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={ - "topics": [{"id": "t1"}, {"id": "t2"}], - }) + if "offset=0" in str(req.url): + return httpx.Response(200, json={ + "topics": [{"id": "t1"}, {"id": "t2"}], + }) + return httpx.Response(200, json={"topics": []}) sc = _make_service_client(handler) items = list(offset_paginate( @@ -269,7 +297,7 @@ def handler(req: httpx.Request) -> httpx.Response: )) assert len(items) == 3 - assert call_count == 2 + assert call_count == 3 def test_offset_increments(self) -> None: captured: list[httpx.Request] = [] @@ -321,6 +349,23 @@ def handler(req: httpx.Request) -> httpx.Response: assert items == [] + def test_server_caps_page_size(self) -> None: + """Server caps pages below the requested limit — offset must + advance by the actual page size and not stop early.""" + ids = ["a", "b", "c", "d", "e"] + + def handler(req: httpx.Request) -> httpx.Response: + from urllib.parse import parse_qs, urlparse + offset = int(parse_qs(urlparse(str(req.url)).query)["offset"][0]) + return httpx.Response(200, json={ + "items": [{"id": x} for x in ids[offset:offset + 2]], + }) + + sc = _make_service_client(handler) + items = list(offset_paginate(sc, "items", items_key="items", limit=100)) + + assert [i["id"] for i in items] == ids + # ====================================================================== # linked_paginate From 4189780e0d6c7b85ae374f4e511b3c23390dda63 Mon Sep 17 00:00:00 2001 From: Valeriia Date: Thu, 11 Jun 2026 11:43:00 +0200 Subject: [PATCH 2/5] refactor(errors): route all failures through the SDK exception family --- src/sdk/core/config.py | 5 +-- src/sdk/core/exceptions/response.py | 10 ++++++ src/sdk/core/opts.py | 7 ++-- src/sdk/core/pagination.py | 8 +++-- src/sdk/core/provider.py | 56 ++++++++++++----------------- src/sdk/core/signer.py | 18 ++++------ tests/unit/core/test_opts.py | 5 +-- tests/unit/core/test_signer.py | 3 +- 8 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/sdk/core/config.py b/src/sdk/core/config.py index cee7399..c28c64a 100644 --- a/src/sdk/core/config.py +++ b/src/sdk/core/config.py @@ -10,6 +10,7 @@ import yaml from sdk.core.auth import AuthConfig, AuthMode +from sdk.core.exceptions import InvalidInputError logger = logging.getLogger(__name__) @@ -30,11 +31,11 @@ def load_from_yaml(cloud_name: str = "otc", file_path: str | Path | None = None) try: data = yaml.safe_load(f) except yaml.YAMLError as e: - raise ValueError(f"Failed to parse YAML file at {path_to_load}: {e}") from e + raise InvalidInputError("clouds.yaml", str(path_to_load)) from e clouds = data.get("clouds", {}) if cloud_name not in clouds: - raise ValueError(f"Cloud '{cloud_name}' not found in {path_to_load}") + raise InvalidInputError("clouds.yaml", str(path_to_load)) cloud_config = clouds[cloud_name] auth_data = cloud_config.get("auth", {}) diff --git a/src/sdk/core/exceptions/response.py b/src/sdk/core/exceptions/response.py index 5bb00a5..cd48055 100644 --- a/src/sdk/core/exceptions/response.py +++ b/src/sdk/core/exceptions/response.py @@ -63,6 +63,16 @@ def _format_message(self) -> str: msg += f"\n{self.body}" return msg +class MalformedResponseError(SDKError): + """API returned 2xx but the body doesn't match the expected shape. + + Args: + detail: What exactly was wrong with the response. + """ + + def __init__(self, detail: str) -> None: + self.detail = detail + super().__init__(f"Malformed API response: {detail}") # --- Status-code specific errors --- diff --git a/src/sdk/core/opts.py b/src/sdk/core/opts.py index 017b9a3..233d508 100644 --- a/src/sdk/core/opts.py +++ b/src/sdk/core/opts.py @@ -6,6 +6,8 @@ from pydantic import BaseModel +from sdk.core.exceptions import InvalidInputError + class BaseOpts(BaseModel): """Base class for request body options. @@ -50,10 +52,7 @@ def to_query_params(self) -> dict[str, str]: if value == "": continue if isinstance(value, (dict, list)): - raise TypeError( - f"Query param {key!r} has non-scalar value {value!r}; " - "query strings only support scalar values." - ) + raise InvalidInputError(key, value) if isinstance(value, bool): params[key] = "true" if value else "false" else: diff --git a/src/sdk/core/pagination.py b/src/sdk/core/pagination.py index 803cecb..1c3a3d3 100644 --- a/src/sdk/core/pagination.py +++ b/src/sdk/core/pagination.py @@ -35,6 +35,8 @@ from typing import Any from urllib.parse import parse_qs, urlencode, urlparse, urlunparse, urljoin +from sdk.core.exceptions import InvalidInputError +from sdk.core.exceptions.response import MalformedResponseError from sdk.core.service_client import ServiceClient T = TypeVar("T", bound=BaseModel) @@ -131,7 +133,7 @@ def offset_paginate( otherwise raw resource dicts. """ if limit <= 0: - raise ValueError("Limit must be strictly positive for offset pagination.") + raise InvalidInputError("limit", limit) query: dict[str, str] = dict(params) if params else {} query["limit"] = str(limit) offset = start_offset @@ -306,6 +308,6 @@ def _fetch_page( data = resp.json() if items_key not in data: - raise ValueError(f"Expected key '{items_key}' not found in API response") - + raise MalformedResponseError( + f"expected key '{items_key}' not found in list response") return data, data[items_key] diff --git a/src/sdk/core/provider.py b/src/sdk/core/provider.py index 13ea265..9dc63d7 100644 --- a/src/sdk/core/provider.py +++ b/src/sdk/core/provider.py @@ -49,7 +49,7 @@ from sdk.core.exceptions import ( ReauthError, UnauthorizedError, - raise_for_status + raise_for_status, AuthError, ResourceNotFoundError ) from sdk.core.signer import SignOptions, sign_request @@ -183,9 +183,8 @@ def authenticate(self) -> None: else: self._aksk_auth() if self.endpoint_locator is None: - raise RuntimeError( - "Endpoint locator not initialized after authentication" - ) + raise AuthError( + "Endpoint locator not initialized after authentication") def request( self, @@ -607,42 +606,33 @@ def _fetch_catalog(self) -> list[CatalogEntry]: raw_catalog = resp.json().get("catalog", []) return [CatalogEntry.model_validate(entry) for entry in raw_catalog] - def _resolve_project_id(self, name: str) -> str: - """Look up project ID by name via IAM API. - - Args: - name: Project name. - - Returns: - Project ID string. + def _resolve_named_id( + self, *, resource: str, url_suffix: str, items_key: str, name: str + ) -> str: + """Look up a resource ID by name via the IAM API. Raises: - EndpointNotFoundError: If no project is found. + ResourceNotFoundError: If nothing matches ``name``. """ resp = self._iam_request( - "GET", - self.identity_v3_endpoint + f"projects?name={name}", + "GET", self.identity_v3_endpoint + f"{url_suffix}?name={name}" ) - data = resp.json() - projects = data.get("projects", []) - if not projects: - raise ValueError(f"Project with name '{name}' not found") - return projects[0]["id"] - - def _resolve_domain_id(self, name: str) -> str: - """Look up domain ID by name via IAM API. + items = resp.json().get(items_key, []) + if not items: + raise ResourceNotFoundError(resource, name) + return items[0]["id"] - Args: - name: Domain name. + def _resolve_project_id(self, name: str) -> str: + return self._resolve_named_id( + resource="Project", url_suffix="projects", + items_key="projects", name=name, + ) - Returns: - Domain ID string, or empty string if not found. - """ - resp = self._iam_request("GET", self.identity_v3_endpoint + f"auth/domains?name={name}") - domains = resp.json().get("domains", []) - if not domains: - raise ValueError(f"Domain with name '{name}' not found") - return domains[0]["id"] + def _resolve_domain_id(self, name: str) -> str: + return self._resolve_named_id( + resource="Domain", url_suffix="auth/domains", + items_key="domains", name=name, + ) # ====================================================================== # Module-level helpers diff --git a/src/sdk/core/signer.py b/src/sdk/core/signer.py index cdb9780..2b31533 100644 --- a/src/sdk/core/signer.py +++ b/src/sdk/core/signer.py @@ -42,6 +42,8 @@ import httpx from pydantic import BaseModel, ConfigDict, computed_field, SecretStr +from sdk.core.exceptions import InvalidInputError + logger = logging.getLogger(__name__) SIGN_ALGORITHM_HMAC_SHA256 = "SDK-HMAC-SHA256" @@ -235,10 +237,7 @@ def _build_sign_params( """ algorithm = opts.sign_algorithm or SIGN_ALGORITHM_HMAC_SHA256 if algorithm not in _SUPPORTED_ALGORITHMS: - raise ValueError( - f"Unsupported signing algorithm '{algorithm}', " - f"supported: {sorted(_SUPPORTED_ALGORITHMS)}" - ) + raise InvalidInputError("sign_algorithm", algorithm) base_time = timestamp if timestamp is not None else datetime.now(UTC) signing_time = base_time - timedelta(seconds=opts.time_offset_seconds) @@ -425,10 +424,7 @@ def _compute_signature(data: str, key: bytes, algorithm: str) -> bytes: """ if algorithm == SIGN_ALGORITHM_HMAC_SHA256: return _hmac_sha256(data, key) - raise ValueError( - f"Unsupported algorithm '{algorithm}', " - f"supported: {sorted(_SUPPORTED_ALGORITHMS)}" - ) + raise InvalidInputError("algorithm", algorithm) def _format_datetime(dt: datetime) -> str: """Format timestamp as ``20060102T150405Z``.""" @@ -450,10 +446,8 @@ def _read_body(request: httpx.Request) -> bytes: try: return request.content or b"" except httpx.RequestNotRead as e: - raise RuntimeError( - "Streaming bodies are not supported for AK/SK signing. " - "The request content must be fully loaded in memory." - ) from e + raise InvalidInputError( + "request.content", "") from e def _use_payload_for_query(request: httpx.Request) -> bool: """Check if query string should be used as payload. diff --git a/tests/unit/core/test_opts.py b/tests/unit/core/test_opts.py index 9063e29..c4fa32c 100644 --- a/tests/unit/core/test_opts.py +++ b/tests/unit/core/test_opts.py @@ -7,6 +7,7 @@ import pytest from pydantic import BaseModel, Field +from sdk.core.exceptions import InvalidInputError from sdk.core.opts import BaseOpts, BaseQueryOpts @@ -155,7 +156,7 @@ def test_query_params_rejects_list_value(): class _BadQuery(BaseQueryOpts): tags: list[str] | None = None - with pytest.raises(TypeError, match="non-scalar"): + with pytest.raises(InvalidInputError): _BadQuery(tags=["a", "b"]).to_query_params() @@ -163,5 +164,5 @@ def test_query_params_rejects_dict_value(): class _BadQuery(BaseQueryOpts): meta: dict[str, str] | None = None - with pytest.raises(TypeError, match="non-scalar"): + with pytest.raises(InvalidInputError): _BadQuery(meta={"k": "v"}).to_query_params() \ No newline at end of file diff --git a/tests/unit/core/test_signer.py b/tests/unit/core/test_signer.py index fea6d33..1935264 100644 --- a/tests/unit/core/test_signer.py +++ b/tests/unit/core/test_signer.py @@ -5,6 +5,7 @@ import httpx import pytest +from sdk.core.exceptions import InvalidInputError from sdk.core.signer import ( SIGN_ALGORITHM_HMAC_SHA256, SignOptions, @@ -308,7 +309,7 @@ def test_unsupported_algorithm_raises(self): sign_algorithm="UNSUPPORTED-ALG", ) req = httpx.Request("GET", "https://example.com/test") - with pytest.raises(ValueError, match="Unsupported"): + with pytest.raises(InvalidInputError): sign_request(req, opts) def test_default_algorithm(self): From f3d5861e118a33f3a45c4599f72e081e84298369 Mon Sep 17 00:00:00 2001 From: Valeriia Date: Thu, 11 Jun 2026 11:53:28 +0200 Subject: [PATCH 3/5] fix(pagination): make generator types honest via overloads --- src/sdk/core/pagination.py | 103 ++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/src/sdk/core/pagination.py b/src/sdk/core/pagination.py index 1c3a3d3..78d7f44 100644 --- a/src/sdk/core/pagination.py +++ b/src/sdk/core/pagination.py @@ -32,7 +32,7 @@ from pydantic import BaseModel from typing import TypeVar from collections.abc import Generator -from typing import Any +from typing import Any, overload from urllib.parse import parse_qs, urlencode, urlparse, urlunparse, urljoin from sdk.core.exceptions import InvalidInputError @@ -40,7 +40,31 @@ from sdk.core.service_client import ServiceClient T = TypeVar("T", bound=BaseModel) -PaginatedItem = T | dict[str, Any] + +@overload +def marker_paginate( + client: ServiceClient, + path: str, + *, + items_key: str, + model: type[T], + marker_key: str = ..., + limit: int = ..., + params: dict[str, str] | None = ..., +) -> Generator[T, None, None]: ... + + +@overload +def marker_paginate( + client: ServiceClient, + path: str, + *, + items_key: str, + model: None = ..., + marker_key: str = ..., + limit: int = ..., + params: dict[str, str] | None = ..., +) -> Generator[dict[str, Any], None, None]: ... def marker_paginate( client: ServiceClient, @@ -51,7 +75,7 @@ def marker_paginate( marker_key: str = "id", limit: int = 0, params: dict[str, str] | None = None, -) -> Generator[PaginatedItem, None, None]: +) -> Generator[Any, None, None]: """Paginate using marker-based strategy. Fetches pages by setting ``marker`` query param to the last @@ -100,6 +124,31 @@ def marker_paginate( query["marker"] = marker_str +@overload +def offset_paginate( + client: ServiceClient, + path: str, + *, + items_key: str, + model: type[T], + limit: int, + start_offset: int = ..., + params: dict[str, str] | None = ..., +) -> Generator[T, None, None]: ... + + +@overload +def offset_paginate( + client: ServiceClient, + path: str, + *, + items_key: str, + model: None = ..., + limit: int, + start_offset: int = ..., + params: dict[str, str] | None = ..., +) -> Generator[dict[str, Any], None, None]: ... + def offset_paginate( client: ServiceClient, path: str, @@ -109,7 +158,7 @@ def offset_paginate( limit: int, start_offset: int = 0, params: dict[str, str] | None = None, -) -> Generator[PaginatedItem, None, None]: +) -> Generator[Any, None, None]: """Paginate using offset-based strategy. Increments ``offset`` by ``limit`` on each page. Stops when @@ -151,6 +200,28 @@ def offset_paginate( offset += len(items) +@overload +def linked_paginate( + client: ServiceClient, + path: str, + *, + items_key: str, + model: type[T], + link_path: list[str] | None = ..., + params: dict[str, str] | None = ..., +) -> Generator[T, None, None]: ... + + +@overload +def linked_paginate( + client: ServiceClient, + path: str, + *, + items_key: str, + model: None = ..., + link_path: list[str] | None = ..., + params: dict[str, str] | None = ..., +) -> Generator[dict[str, Any], None, None]: ... def linked_paginate( client: ServiceClient, @@ -160,7 +231,7 @@ def linked_paginate( model: type[T] | None = None, link_path: list[str] | None = None, params: dict[str, str] | None = None, -) -> Generator[PaginatedItem, None, None]: +) -> Generator[Any, None, None]: """Paginate using linked (next URL) strategy. Follows a ``next`` link embedded in the response body. @@ -208,6 +279,26 @@ def linked_paginate( return url = urljoin(url, next_url) +@overload +def single_page( + client: ServiceClient, + path: str, + *, + items_key: str, + model: type[T], + params: dict[str, str] | None = ..., +) -> list[T]: ... + + +@overload +def single_page( + client: ServiceClient, + path: str, + *, + items_key: str, + model: None = ..., + params: dict[str, str] | None = ..., +) -> list[dict[str, Any]]: ... def single_page( client: ServiceClient, @@ -216,7 +307,7 @@ def single_page( items_key: str, model: type[T] | None = None, params: dict[str, str] | None = None, -) -> list[PaginatedItem]: +) -> list[Any]: """Fetch a single (non-paginated) list response. Convenience wrapper for endpoints that return all items From 57f2565a2ecdc625d97c3ca88a3cd6404e61ca93 Mon Sep 17 00:00:00 2001 From: Valeriia Date: Thu, 11 Jun 2026 12:15:15 +0200 Subject: [PATCH 4/5] refactor(vpc): single source for resource keys; paginator owns paging params --- src/sdk/core/pagination.py | 3 +-- src/sdk/services/vpc/v1/vpcs/common.py | 6 ++++++ src/sdk/services/vpc/v1/vpcs/create.py | 7 ++----- src/sdk/services/vpc/v1/vpcs/list.py | 3 +++ src/sdk/services/vpc/v1/vpcs/update.py | 5 ++--- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/sdk/core/pagination.py b/src/sdk/core/pagination.py index 78d7f44..f4ea340 100644 --- a/src/sdk/core/pagination.py +++ b/src/sdk/core/pagination.py @@ -30,9 +30,8 @@ from __future__ import annotations from pydantic import BaseModel -from typing import TypeVar from collections.abc import Generator -from typing import Any, overload +from typing import Any, overload, TypeVar from urllib.parse import parse_qs, urlencode, urlparse, urlunparse, urljoin from sdk.core.exceptions import InvalidInputError diff --git a/src/sdk/services/vpc/v1/vpcs/common.py b/src/sdk/services/vpc/v1/vpcs/common.py index c138a46..396d088 100644 --- a/src/sdk/services/vpc/v1/vpcs/common.py +++ b/src/sdk/services/vpc/v1/vpcs/common.py @@ -3,10 +3,16 @@ from __future__ import annotations from pydantic import BaseModel, Field +from typing import ClassVar +from sdk.core.opts import BaseOpts _BASE_PATH = "vpcs" +class VpcBaseOpts(BaseOpts): + _wrapper_key: ClassVar[str | None] = "vpc" + + class Route(BaseModel): """VPC route entry.""" diff --git a/src/sdk/services/vpc/v1/vpcs/create.py b/src/sdk/services/vpc/v1/vpcs/create.py index 4ca157e..f5c15bb 100644 --- a/src/sdk/services/vpc/v1/vpcs/create.py +++ b/src/sdk/services/vpc/v1/vpcs/create.py @@ -4,20 +4,17 @@ from typing import ClassVar -from sdk.core.opts import BaseOpts from sdk.core.service_client import ServiceClient -from .common import _BASE_PATH, Vpc +from .common import _BASE_PATH, Vpc, VpcBaseOpts -class CreateVpcOpts(BaseOpts): +class CreateVpcOpts(VpcBaseOpts): """Options for creating a VPC. All fields are optional per the API spec. """ - _wrapper_key: ClassVar[str | None] = "vpc" - name: str | None = None description: str | None = None cidr: str | None = None diff --git a/src/sdk/services/vpc/v1/vpcs/list.py b/src/sdk/services/vpc/v1/vpcs/list.py index e358225..4eaa23f 100644 --- a/src/sdk/services/vpc/v1/vpcs/list.py +++ b/src/sdk/services/vpc/v1/vpcs/list.py @@ -32,6 +32,9 @@ def list( # noqa: A001 - shadows builtin intentionally; matches Go SDK style fetching next pages automatically. """ params = opts.to_query_params() if opts else None + if params: + params.pop("limit", None) + params.pop("marker", None) limit = opts.limit if (opts and opts.limit and opts.limit > 0) else 0 return marker_paginate( diff --git a/src/sdk/services/vpc/v1/vpcs/update.py b/src/sdk/services/vpc/v1/vpcs/update.py index a1a67c4..ae31a3f 100644 --- a/src/sdk/services/vpc/v1/vpcs/update.py +++ b/src/sdk/services/vpc/v1/vpcs/update.py @@ -7,17 +7,16 @@ from sdk.core.opts import BaseOpts from sdk.core.service_client import ServiceClient -from .common import _BASE_PATH, Route, Vpc +from .common import _BASE_PATH, Route, Vpc, VpcBaseOpts -class UpdateVpcOpts(BaseOpts): +class UpdateVpcOpts(VpcBaseOpts): """Options for updating a VPC. All fields are optional. ``None`` means "do not touch", an explicit empty string clears the field on the server. """ - _wrapper_key: ClassVar[str | None] = "vpc" name: str | None = None description: str | None = None From ba74a37679b790a8a6f9c6e1e397de945bac549c Mon Sep 17 00:00:00 2001 From: Valeriia Date: Thu, 11 Jun 2026 12:17:22 +0200 Subject: [PATCH 5/5] refactor(vpc): single source for resource keys; paginator owns paging params --- src/sdk/services/vpc/v1/vpcs/list.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sdk/services/vpc/v1/vpcs/list.py b/src/sdk/services/vpc/v1/vpcs/list.py index 4eaa23f..e358225 100644 --- a/src/sdk/services/vpc/v1/vpcs/list.py +++ b/src/sdk/services/vpc/v1/vpcs/list.py @@ -32,9 +32,6 @@ def list( # noqa: A001 - shadows builtin intentionally; matches Go SDK style fetching next pages automatically. """ params = opts.to_query_params() if opts else None - if params: - params.pop("limit", None) - params.pop("marker", None) limit = opts.limit if (opts and opts.limit and opts.limit > 0) else 0 return marker_paginate(