-
Notifications
You must be signed in to change notification settings - Fork 0
Fix/v4 reviews #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0d73429
8dc1953
0ff5b78
1cb8280
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| from .constants import API_MAX_RETRIES, API_TIMEOUT, OPENAPI_PREFIX | ||
| from .errors import ( | ||
| AuthenticationError, | ||
| InvalidParameter, | ||
| NetworkError, | ||
| RateLimitError, | ||
| RequestTimeoutError, | ||
|
|
@@ -50,6 +51,9 @@ | |
| # HTTP methods that are safe to retry without risking duplicate side-effects. | ||
| _IDEMPOTENT_METHODS: frozenset[str] = frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE"}) | ||
|
|
||
| # POST endpoints that are semantically idempotent (deploy/stop are state transitions). | ||
| _RETRYABLE_POST_PATHS: frozenset[str] = frozenset({"/deploy", "/stop", "/undeploy"}) | ||
|
|
||
| # Errors that warrant a transparent retry. | ||
| _RETRYABLE_EXC: tuple[type[BaseException], ...] = ( | ||
| NetworkError, ServerError, RateLimitError, | ||
|
|
@@ -225,8 +229,12 @@ def _request( | |
| else: | ||
| return self._decode(response, unwrap=unwrap) | ||
|
|
||
| # Retry policy: only for idempotent methods on transient errors. | ||
| if attempt >= attempts or method_upper not in _IDEMPOTENT_METHODS: | ||
| # Retry policy: idempotent methods + known-idempotent POST paths. | ||
| is_retryable = ( | ||
| method_upper in _IDEMPOTENT_METHODS | ||
| or (method_upper == "POST" and any(path.endswith(p) for p in _RETRYABLE_POST_PATHS)) | ||
| ) | ||
| if attempt >= attempts or not is_retryable: | ||
| break | ||
| backoff = min(2 ** (attempt - 1), 16) | ||
| _logger.debug( | ||
|
|
@@ -271,14 +279,18 @@ def list_models( | |
| owner: str | None = None, | ||
| sort: str | None = None, | ||
| page_number: int = 1, | ||
| page_size: int = 20, | ||
| page_size: int = 10, | ||
| filters: Filters = None, | ||
| ) -> JSON: | ||
| """``GET /models`` — list models with pagination and filters. | ||
|
|
||
| Supported filter keys: ``task``, ``library``, ``model_type``, | ||
| ``custom_tag``, ``license``, ``deploy``. | ||
| """ | ||
| if page_number * page_size > 3000: | ||
| raise InvalidParameter( | ||
| f"page_number * page_size must be <= 3000 (got {page_number * page_size})." | ||
| ) | ||
| params = self._merge_params( | ||
| { | ||
| "search": search, | ||
|
|
@@ -305,10 +317,14 @@ def list_datasets( | |
| owner: str | None = None, | ||
| sort: str | None = None, | ||
| page_number: int = 1, | ||
| page_size: int = 20, | ||
| page_size: int = 10, | ||
| filters: Filters = None, | ||
| ) -> JSON: | ||
| """``GET /datasets`` — list datasets. Filter keys: ``task``, ``license``.""" | ||
| if page_number * page_size > 3000: | ||
| raise InvalidParameter( | ||
| f"page_number * page_size must be <= 3000 (got {page_number * page_size})." | ||
| ) | ||
|
Comment on lines
+324
to
+327
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pagination parameters if page_number < 1 or page_size < 1:
raise InvalidParameter("page_number and page_size must be positive integers.")
if page_number * page_size > 3000:
raise InvalidParameter(
f"page_number * page_size must be <= 3000 (got {page_number * page_size})."
) |
||
| params = self._merge_params( | ||
| { | ||
| "search": search, | ||
|
|
@@ -379,14 +395,18 @@ def list_skills( | |
| *, | ||
| search: str | None = None, | ||
| page_number: int = 1, | ||
| page_size: int = 20, | ||
| page_size: int = 10, | ||
| filters: Filters = None, | ||
| ) -> JSON: | ||
| """``GET /skills`` — list skills. | ||
|
|
||
| Filter keys: ``developer``, ``category``, ``license``, ``custom_tag``, | ||
| ``owner``. | ||
| """ | ||
| if page_number * page_size > 3000: | ||
| raise InvalidParameter( | ||
| f"page_number * page_size must be <= 3000 (got {page_number * page_size})." | ||
| ) | ||
|
Comment on lines
+406
to
+409
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pagination parameters if page_number < 1 or page_size < 1:
raise InvalidParameter("page_number and page_size must be positive integers.")
if page_number * page_size > 3000:
raise InvalidParameter(
f"page_number * page_size must be <= 3000 (got {page_number * page_size})."
) |
||
| params = self._merge_params( | ||
| { | ||
| "search": search, | ||
|
|
@@ -427,7 +447,7 @@ def create_studio(self, payload: CreateStudioPayload | Mapping[str, Any]) -> JSO | |
|
|
||
| def get_studio(self, owner: str, repo_name: str) -> JSON: | ||
| """``GET /studios/{owner}/{repo_name}`` — fetch Studio metadata.""" | ||
| return self._request("GET", f"/studios/{owner}/{repo_name}", require_token=False) | ||
| return self._request("GET", f"/studios/{owner}/{repo_name}") | ||
|
|
||
| def deploy_studio( | ||
| self, | ||
|
|
@@ -439,12 +459,12 @@ def deploy_studio( | |
| return self._request( | ||
| "POST", | ||
| f"/studios/{owner}/{repo_name}/deploy", | ||
| json_body=dict(payload or {}), | ||
| json_body=dict(payload) if payload else None, | ||
| ) | ||
|
|
||
| def stop_studio(self, owner: str, repo_name: str) -> JSON: | ||
| """``POST /studios/{owner}/{repo_name}/stop`` — stop a running Studio.""" | ||
| return self._request("POST", f"/studios/{owner}/{repo_name}/stop") | ||
| return self._request("POST", f"/studios/{owner}/{repo_name}/stop", json_body=None) | ||
|
|
||
| def get_studio_logs( | ||
| self, | ||
|
|
@@ -523,15 +543,28 @@ def list_mcp_servers( | |
| *, | ||
| search: str | None = None, | ||
| page_number: int = 1, | ||
| page_size: int = 20, | ||
| page_size: int = 10, | ||
| filter: Mapping[str, Any] | None = None, | ||
| extra: Mapping[str, Any] | None = None, | ||
| ) -> JSON: | ||
| """``PUT /mcp/servers`` — discover MCP servers (JSON body, not query).""" | ||
| """``PUT /mcp/servers`` — discover MCP servers (JSON body, not query). | ||
|
|
||
| Parameters | ||
| ---------- | ||
| filter : dict, optional | ||
| Nested filter object. Supported keys: ``category``, ``is_hosted``. | ||
| """ | ||
| if page_number * page_size > 100: | ||
| raise InvalidParameter( | ||
| f"page_number * page_size must be <= 100 for MCP servers (got {page_number * page_size})." | ||
| ) | ||
|
Comment on lines
+557
to
+560
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pagination parameters if page_number < 1 or page_size < 1:
raise InvalidParameter("page_number and page_size must be positive integers.")
if page_number * page_size > 100:
raise InvalidParameter(
f"page_number * page_size must be <= 100 for MCP servers (got {page_number * page_size})."
) |
||
| body: dict[str, Any] = { | ||
| "search": search, | ||
| "page_number": page_number, | ||
| "page_size": page_size, | ||
| } | ||
| if filter: | ||
| body["filter"] = dict(filter) | ||
| if extra: | ||
| body.update(extra) | ||
| body = {k: v for k, v in body.items() if v is not None} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pagination parameters
page_numberandpage_sizeare multiplied to check if they exceed the limit of 3000. However, if either parameter is negative or zero, the checkpage_number * page_size > 3000can be bypassed (e.g.,-1 * 10 = -10 <= 3000). To prevent sending invalid pagination values to the backend, we should explicitly validate that bothpage_numberandpage_sizeare positive integers (greater than or equal to 1).