diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f404495..794ba02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,7 @@ repos: dependency-injector>=4.41.0, inquirer>=3.1.0, aiohttp>=3.8.0, + aiohttp-retry>=2.8.0, python-dateutil>=2.8.0, typing-extensions>=4.0.0, pytest>=7.0.0, diff --git a/openapi-config.yaml b/openapi-config.yaml index 1530afc..56fb6ce 100644 --- a/openapi-config.yaml +++ b/openapi-config.yaml @@ -5,3 +5,4 @@ packageVersion: 1.0.0 packageUrl: https://github.com/workato/workato-platform-cli hideGenerationTimestamp: true generateSourceCodeOnly: true +lazyImports: false diff --git a/pyproject.toml b/pyproject.toml index 343a243..2775e9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -236,7 +236,7 @@ dev = [ "build>=1.3.0", "hatch-vcs>=0.5.0", "mypy>=1.17.1", - "openapi-generator-cli>=7.15.0", + "openapi-generator-cli>=7.16.0", "pip-audit>=2.9.0", "pre-commit>=4.3.0", "pytest>=7.0.0", diff --git a/src/.openapi-generator/VERSION b/src/.openapi-generator/VERSION index e465da4..971ecb2 100644 --- a/src/.openapi-generator/VERSION +++ b/src/.openapi-generator/VERSION @@ -1 +1 @@ -7.14.0 +7.16.0 diff --git a/src/workato_platform/__init__.py b/src/workato_platform/__init__.py index 86b5e26..88d7111 100644 --- a/src/workato_platform/__init__.py +++ b/src/workato_platform/__init__.py @@ -4,6 +4,8 @@ from typing import Any +import aiohttp_retry + try: from workato_platform._version import __version__ @@ -25,6 +27,46 @@ from workato_platform.client.workato_api.configuration import Configuration +def _configure_retry_with_429_support( + rest_client: Any, configuration: Configuration +) -> None: + """ + Configure REST client to retry on 429 (Too Many Requests) errors. + + This patches the auto-generated REST client to add 429 to the list of + retryable status codes with appropriate exponential backoff for rate limiting. + + Args: + rest_client: The RESTClientObject instance to configure + configuration: The Configuration object containing retry settings + """ + # Enable retries by default if not explicitly set + if configuration.retries is None: + configuration.retries = 3 + + # Update the retries setting on the REST client + rest_client.retries = configuration.retries + + # Force recreation of retry_client with 429 support + if rest_client.retry_client is not None: + rest_client.retry_client = None + + # The retry_client will be lazily created on first request with our patched settings + # We need to pre-create it here to ensure 429 support + if rest_client.retries is not None: + rest_client.retry_client = aiohttp_retry.RetryClient( + client_session=rest_client.pool_manager, + retry_options=aiohttp_retry.ExponentialRetry( + attempts=rest_client.retries, + factor=2.0, + start_timeout=1.0, # Increased from default 0.1s for rate limiting + max_timeout=120.0, # 2 minutes max + statuses={429}, # Add 429 Too Many Requests + retry_all_server_errors=True, # Keep 5xx errors + ), + ) + + class Workato: """Wrapper class that provides easy access to all Workato API endpoints.""" @@ -45,6 +87,9 @@ def __init__(self, configuration: Configuration): ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 ) + # Configure retry logic with 429 (Too Many Requests) support + _configure_retry_with_429_support(rest_client, configuration) + # Initialize all API endpoints self.projects_api = ProjectsApi(self._api_client) self.properties_api = PropertiesApi(self._api_client) diff --git a/src/workato_platform/cli/commands/recipes/validator.py b/src/workato_platform/cli/commands/recipes/validator.py index 2932c57..b6447fb 100644 --- a/src/workato_platform/cli/commands/recipes/validator.py +++ b/src/workato_platform/cli/commands/recipes/validator.py @@ -430,6 +430,7 @@ def _load_cached_connectors(self) -> bool: # Check cache age cache_age = time.time() - self._cache_file.stat().st_mtime + if cache_age > (self._cache_ttl_hours * 3600): return False # Cache expired @@ -581,21 +582,30 @@ async def _load_builtin_connectors(self) -> None: provider_name = platform_connector.name.lower() self.known_adapters.add(provider_name) + # Convert List[ConnectorAction] to dict for JSON serialization + # and validation. The validation logic expects dicts (calls .keys()), + # so we convert: List[ConnectorAction] -> {action.name: action.to_dict()} self.connector_metadata[provider_name] = { "type": "platform", "name": platform_connector.name, "deprecated": platform_connector.deprecated, "categories": platform_connector.categories, - "triggers": platform_connector.triggers, - "actions": platform_connector.actions, + "triggers": { + trigger.name: trigger.to_dict() + for trigger in platform_connector.triggers + }, + "actions": { + action.name: action.to_dict() + for action in platform_connector.actions + }, } # Fetch custom connectors - customer_connectores_response = ( + custom_connectors_response = ( await self.workato_api_client.connectors_api.list_custom_connectors() ) - for custom_connector in customer_connectores_response.result: + for custom_connector in custom_connectors_response.result: provider_name = custom_connector.name.lower() code_response = ( await self.workato_api_client.connectors_api.get_custom_connector_code( @@ -603,12 +613,15 @@ async def _load_builtin_connectors(self) -> None: ) ) self.known_adapters.add(provider_name) + # Note: Custom connector trigger/action parsing is not implemented + # Using empty dicts for consistency with platform connector structure self.connector_metadata[provider_name] = { "type": "custom", "name": custom_connector.name, "code": code_response.data.code, - "triggers": [], # Not Implemented - "actions": [], # Not Implemented + "categories": [], # Custom connectors don't have categories + "triggers": {}, # Not Implemented - would need to parse code + "actions": {}, # Not Implemented - would need to parse code } # Save to cache for next time @@ -1225,26 +1238,70 @@ def check_value(value: Any, field_path: list[str] | None = None) -> None: # Parse the JSON structure dp_data = json.loads(dp_json) - # Validate required fields - required_fields = ["pill_type", "provider", "line", "path"] - for required_field in required_fields: - if required_field not in dp_data: - errors.append( - ValidationError( - message=( - f"Data pill missing required field " - f"'{required_field}' in step " - f"{line_number}" - ), - error_type=ErrorType.INPUT_INVALID_BY_ADAPTER, - line_number=line_number, - field_path=field_path + ["_dp"], - ) + # Validate required fields based on pill_type + pill_type = dp_data.get("pill_type") + + # All data pills must have pill_type + if not pill_type: + errors.append( + ValidationError( + message=( + "Data pill missing required field " + f"'pill_type' in step {line_number}" + ), + error_type=ErrorType.INPUT_INVALID_BY_ADAPTER, + line_number=line_number, + field_path=field_path + ["_dp"], ) + ) + else: + # Validate based on pill_type + if pill_type in ("output", "refs"): + # Output/refs pills need provider, line, path + required = ["provider", "line", "path"] + for field in required: + if field not in dp_data: + errors.append( + ValidationError( + message=( + f"Data pill with pill_type " + f"'{pill_type}' missing " + f"required field '{field}' " + f"in step {line_number}" + ), + error_type=( + ErrorType.INPUT_INVALID_BY_ADAPTER + ), + line_number=line_number, + field_path=field_path + ["_dp"], + ) + ) + elif pill_type == "project_property": + # Project property pills need property_name + if "property_name" not in dp_data: + errors.append( + ValidationError( + message=( + "Data pill with pill_type " + "'project_property' missing " + f"required field 'property_name' " + f"in step {line_number}" + ), + error_type=( + ErrorType.INPUT_INVALID_BY_ADAPTER + ), + line_number=line_number, + field_path=field_path + ["_dp"], + ) + ) + # Other pill types (e.g., "lookup", "variable") + # can be added here as needed - # Validate provider/line references exist + # Validate provider/line references exist (only for + # output/refs pills) if ( - "provider" in dp_data + pill_type in ("output", "refs") + and "provider" in dp_data and "line" in dp_data and not self._step_exists( dp_data["provider"], dp_data["line"] @@ -1264,9 +1321,11 @@ def check_value(value: Any, field_path: list[str] | None = None) -> None: ) ) - # Validate path is an array - if "path" in dp_data and not isinstance( - dp_data["path"], list + # Validate path is an array (only for output/refs pills) + if ( + pill_type in ("output", "refs") + and "path" in dp_data + and not isinstance(dp_data["path"], list) ): errors.append( ValidationError( diff --git a/src/workato_platform/client/workato_api/__init__.py b/src/workato_platform/client/workato_api/__init__.py index a328b6c..279a191 100644 --- a/src/workato_platform/client/workato_api/__init__.py +++ b/src/workato_platform/client/workato_api/__init__.py @@ -199,3 +199,4 @@ from workato_platform.client.workato_api.models.user import User as User from workato_platform.client.workato_api.models.validation_error import ValidationError as ValidationError from workato_platform.client.workato_api.models.validation_error_errors_value import ValidationErrorErrorsValue as ValidationErrorErrorsValue + diff --git a/src/workato_platform/client/workato_api/api_client.py b/src/workato_platform/client/workato_api/api_client.py index dcb076b..ea849af 100644 --- a/src/workato_platform/client/workato_api/api_client.py +++ b/src/workato_platform/client/workato_api/api_client.py @@ -21,6 +21,7 @@ import os import re import tempfile +import uuid from urllib.parse import quote from typing import Tuple, Optional, List, Dict, Union @@ -359,6 +360,8 @@ def sanitize_for_serialization(self, obj): return obj.get_secret_value() elif isinstance(obj, self.PRIMITIVE_TYPES): return obj + elif isinstance(obj, uuid.UUID): + return str(obj) elif isinstance(obj, list): return [ self.sanitize_for_serialization(sub_obj) for sub_obj in obj @@ -411,7 +414,7 @@ def deserialize(self, response_text: str, response_type: str, content_type: Opti data = json.loads(response_text) except ValueError: data = response_text - elif re.match(r'^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): + elif re.match(r'^application/(json|[\w!#$&.+\-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): if response_text == "": data = "" else: @@ -460,13 +463,13 @@ def __deserialize(self, data, klass): if klass in self.PRIMITIVE_TYPES: return self.__deserialize_primitive(data, klass) - elif klass == object: + elif klass is object: return self.__deserialize_object(data) - elif klass == datetime.date: + elif klass is datetime.date: return self.__deserialize_date(data) - elif klass == datetime.datetime: + elif klass is datetime.datetime: return self.__deserialize_datetime(data) - elif klass == decimal.Decimal: + elif klass is decimal.Decimal: return decimal.Decimal(data) elif issubclass(klass, Enum): return self.__deserialize_enum(data, klass) diff --git a/src/workato_platform/client/workato_api/docs/ConnectorAction.md b/src/workato_platform/client/workato_api/docs/ConnectorAction.md index 760d8cc..dbc4713 100644 --- a/src/workato_platform/client/workato_api/docs/ConnectorAction.md +++ b/src/workato_platform/client/workato_api/docs/ConnectorAction.md @@ -6,7 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **str** | | -**title** | **str** | | +**title** | **str** | | [optional] **deprecated** | **bool** | | **bulk** | **bool** | | **batch** | **bool** | | diff --git a/src/workato_platform/client/workato_api/models/__init__.py b/src/workato_platform/client/workato_api/models/__init__.py index e0ad6ae..28c8fe0 100644 --- a/src/workato_platform/client/workato_api/models/__init__.py +++ b/src/workato_platform/client/workato_api/models/__init__.py @@ -12,7 +12,6 @@ Do not edit the class manually. """ # noqa: E501 - # import models into model package from workato_platform.client.workato_api.models.api_client import ApiClient from workato_platform.client.workato_api.models.api_client_api_collections_inner import ApiClientApiCollectionsInner @@ -81,3 +80,4 @@ from workato_platform.client.workato_api.models.user import User from workato_platform.client.workato_api.models.validation_error import ValidationError from workato_platform.client.workato_api.models.validation_error_errors_value import ValidationErrorErrorsValue + diff --git a/src/workato_platform/client/workato_api/models/connector_action.py b/src/workato_platform/client/workato_api/models/connector_action.py index 34a9726..7fa08f3 100644 --- a/src/workato_platform/client/workato_api/models/connector_action.py +++ b/src/workato_platform/client/workato_api/models/connector_action.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self @@ -27,7 +27,7 @@ class ConnectorAction(BaseModel): ConnectorAction """ # noqa: E501 name: StrictStr - title: StrictStr + title: Optional[StrictStr] = None deprecated: StrictBool bulk: StrictBool batch: StrictBool @@ -72,6 +72,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # set to None if title (nullable) is None + # and model_fields_set contains the field + if self.title is None and "title" in self.model_fields_set: + _dict['title'] = None + return _dict @classmethod diff --git a/src/workato_platform/client/workato_api/models/data_table.py b/src/workato_platform/client/workato_api/models/data_table.py index daac153..dd1048c 100644 --- a/src/workato_platform/client/workato_api/models/data_table.py +++ b/src/workato_platform/client/workato_api/models/data_table.py @@ -20,6 +20,7 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List +from uuid import UUID from workato_platform.client.workato_api.models.data_table_column import DataTableColumn from typing import Optional, Set from typing_extensions import Self @@ -28,7 +29,7 @@ class DataTable(BaseModel): """ DataTable """ # noqa: E501 - id: StrictStr + id: UUID name: StrictStr var_schema: List[DataTableColumn] = Field(alias="schema") folder_id: StrictInt diff --git a/src/workato_platform/client/workato_api/models/data_table_column.py b/src/workato_platform/client/workato_api/models/data_table_column.py index e9eb3ea..3bb7b48 100644 --- a/src/workato_platform/client/workato_api/models/data_table_column.py +++ b/src/workato_platform/client/workato_api/models/data_table_column.py @@ -19,6 +19,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator from typing import Any, ClassVar, Dict, List, Optional +from uuid import UUID from workato_platform.client.workato_api.models.data_table_relation import DataTableRelation from typing import Optional, Set from typing_extensions import Self @@ -30,7 +31,7 @@ class DataTableColumn(BaseModel): type: StrictStr name: StrictStr optional: StrictBool - field_id: StrictStr + field_id: UUID hint: Optional[StrictStr] default_value: Optional[Any] = Field(description="Default value matching the column type") metadata: Dict[str, Any] diff --git a/src/workato_platform/client/workato_api/models/data_table_relation.py b/src/workato_platform/client/workato_api/models/data_table_relation.py index 86a4e00..4e60838 100644 --- a/src/workato_platform/client/workato_api/models/data_table_relation.py +++ b/src/workato_platform/client/workato_api/models/data_table_relation.py @@ -17,8 +17,9 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, StrictStr +from pydantic import BaseModel, ConfigDict from typing import Any, ClassVar, Dict, List +from uuid import UUID from typing import Optional, Set from typing_extensions import Self @@ -26,8 +27,8 @@ class DataTableRelation(BaseModel): """ DataTableRelation """ # noqa: E501 - table_id: StrictStr - field_id: StrictStr + table_id: UUID + field_id: UUID __properties: ClassVar[List[str]] = ["table_id", "field_id"] model_config = ConfigDict( diff --git a/src/workato_platform/client/workato_api_README.md b/src/workato_platform/client/workato_api_README.md index ec0490f..da7d2f2 100644 --- a/src/workato_platform/client/workato_api_README.md +++ b/src/workato_platform/client/workato_api_README.md @@ -5,7 +5,7 @@ The `workato_platform.client.workato_api` package is automatically generated by - API version: 1.0.0 - Package version: 1.0.0 -- Generator version: 7.14.0 +- Generator version: 7.16.0 - Build package: org.openapitools.codegen.languages.PythonClientCodegen For more information, please visit [https://docs.workato.com](https://docs.workato.com) diff --git a/tests/unit/commands/recipes/test_validator.py b/tests/unit/commands/recipes/test_validator.py index 5f6e398..a25ce85 100644 --- a/tests/unit/commands/recipes/test_validator.py +++ b/tests/unit/commands/recipes/test_validator.py @@ -1110,11 +1110,21 @@ async def test_load_builtin_connectors_from_api(validator: RecipeValidator) -> N """Test loading connectors from API when cache fails""" # Mock API responses mock_platform_response = MagicMock() + + # Create mock ConnectorAction objects + mock_trigger = Mock() + mock_trigger.name = "webhook" + mock_trigger.to_dict = Mock(return_value={"name": "webhook", "title": "Webhook"}) + + mock_action = Mock() + mock_action.name = "get" + mock_action.to_dict = Mock(return_value={"name": "get", "title": "GET request"}) + platform_connector = Mock( deprecated=False, categories=["Data"], - triggers={"webhook": {}}, - actions={"get": {}}, + triggers=[mock_trigger], + actions=[mock_action], ) platform_connector.name = "HTTP" mock_platform_response.items = [platform_connector] diff --git a/tests/unit/test_retry_429.py b/tests/unit/test_retry_429.py new file mode 100644 index 0000000..874f90e --- /dev/null +++ b/tests/unit/test_retry_429.py @@ -0,0 +1,215 @@ +"""Tests for 429 retry configuration in Workato API client.""" + +from unittest.mock import Mock, patch + +import pytest + + +class TestRetry429Configuration: + """Test that 429 (Too Many Requests) errors are properly configured for retry.""" + + def test_retries_enabled_by_default(self) -> None: + """Test that retries are enabled by default when not explicitly set.""" + try: + from workato_platform import Workato + + with patch("workato_platform.ApiClient") as mock_api_client: + mock_configuration = Mock() + mock_configuration.retries = None # Not explicitly set + + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + + Workato(mock_configuration) + + # Should be set to default value of 3 + assert mock_configuration.retries == 3 + assert mock_rest_client.retries == 3 + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") + + def test_custom_retry_count_preserved(self) -> None: + """Test that explicitly set retry count is preserved.""" + try: + from workato_platform import Workato + + with patch("workato_platform.ApiClient") as mock_api_client: + mock_configuration = Mock() + mock_configuration.retries = 5 # Custom value + + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + + Workato(mock_configuration) + + # Should keep custom value + assert mock_configuration.retries == 5 + assert mock_rest_client.retries == 5 + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") + + def test_retry_client_created_with_429_support(self) -> None: + """Test that retry client is created with 429 status code support.""" + try: + from workato_platform import Workato + + with ( + patch("workato_platform.ApiClient") as mock_api_client, + patch("aiohttp_retry.RetryClient"), + patch("aiohttp_retry.ExponentialRetry") as mock_exponential_retry, + ): + mock_configuration = Mock() + mock_configuration.retries = None + + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + + Workato(mock_configuration) + + # Verify ExponentialRetry was called with correct parameters + mock_exponential_retry.assert_called_once() + call_kwargs = mock_exponential_retry.call_args[1] + + assert call_kwargs["attempts"] == 3 + assert call_kwargs["factor"] == 2.0 + assert call_kwargs["start_timeout"] == 1.0 + assert call_kwargs["max_timeout"] == 120.0 + assert call_kwargs["statuses"] == {429} + assert call_kwargs["retry_all_server_errors"] is True + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") + + def test_exponential_backoff_timing(self) -> None: + """Test that exponential backoff uses correct timing for rate limiting.""" + try: + from workato_platform import Workato + + with ( + patch("workato_platform.ApiClient") as mock_api_client, + patch("aiohttp_retry.ExponentialRetry") as mock_exponential_retry, + ): + mock_configuration = Mock() + mock_configuration.retries = 3 + + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + + Workato(mock_configuration) + + # Verify timing parameters + call_kwargs = mock_exponential_retry.call_args[1] + assert call_kwargs["start_timeout"] == 1.0 # 1 second (not 0.1s) + assert call_kwargs["max_timeout"] == 120.0 # 2 minutes + assert call_kwargs["factor"] == 2.0 # 2x backoff + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") + + def test_configure_retry_helper_function(self) -> None: + """Test the _configure_retry_with_429_support helper function directly.""" + try: + from workato_platform import _configure_retry_with_429_support + + with ( + patch("aiohttp_retry.RetryClient") as mock_retry_client, + patch("aiohttp_retry.ExponentialRetry"), + ): + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + + mock_configuration = Mock() + mock_configuration.retries = None + + _configure_retry_with_429_support(mock_rest_client, mock_configuration) + + # Verify retries were set to default + assert mock_configuration.retries == 3 + assert mock_rest_client.retries == 3 + + # Verify retry client was created + mock_retry_client.assert_called_once() + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") + + def test_retry_client_recreated_if_exists(self) -> None: + """Test that existing retry_client is recreated with new config.""" + try: + from workato_platform import _configure_retry_with_429_support + + with ( + patch("aiohttp_retry.RetryClient") as mock_retry_client, + ): + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = Mock() # Pre-existing retry client + + mock_configuration = Mock() + mock_configuration.retries = 5 + + _configure_retry_with_429_support(mock_rest_client, mock_configuration) + + # Old retry_client should be set to None + # New retry_client should be created + mock_retry_client.assert_called_once() + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") + + def test_server_errors_still_retried(self) -> None: + """Test that 5xx server errors are still retried alongside 429.""" + try: + from workato_platform import Workato + + with ( + patch("workato_platform.ApiClient") as mock_api_client, + patch("aiohttp_retry.ExponentialRetry") as mock_exponential_retry, + ): + mock_configuration = Mock() + mock_configuration.retries = 3 + + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + + Workato(mock_configuration) + + # Verify retry_all_server_errors is True + call_kwargs = mock_exponential_retry.call_args[1] + assert call_kwargs["retry_all_server_errors"] is True + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") diff --git a/tests/unit/test_workato_client.py b/tests/unit/test_workato_client.py index b5632a0..ea5ea56 100644 --- a/tests/unit/test_workato_client.py +++ b/tests/unit/test_workato_client.py @@ -64,14 +64,24 @@ def test_workato_api_endpoints_structure(self) -> None: # Create mock configuration with proper SSL attributes with ( patch("workato_platform.Configuration") as mock_config, + patch("workato_platform.ApiClient") as mock_api_client, ): mock_configuration = Mock() mock_configuration.connection_pool_maxsize = 10 mock_configuration.ssl_ca_cert = None mock_configuration.ca_cert_data = None mock_configuration.cert_file = None + mock_configuration.retries = None mock_config.return_value = mock_configuration + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + client = Workato(mock_configuration) # Check that expected API endpoints exist as attributes @@ -276,3 +286,36 @@ def test_workato_all_api_endpoints_initialized(self) -> None: except ImportError: pytest.skip("Workato class not available due to missing dependencies") + + def test_workato_retry_429_configured(self) -> None: + """Test that retry logic with 429 support is configured.""" + try: + from workato_platform import Workato + + with ( + patch("workato_platform.ApiClient") as mock_api_client, + patch("aiohttp_retry.RetryClient") as mock_retry_client, + ): + mock_configuration = Mock() + mock_configuration.retries = None # Should default to 3 + + mock_client_instance = Mock() + mock_rest_client = Mock() + mock_rest_client.pool_manager = Mock() + mock_rest_client.retry_client = None + mock_rest_client.ssl_context = Mock() + + mock_api_client.return_value = mock_client_instance + mock_client_instance.rest_client = mock_rest_client + + Workato(mock_configuration) + + # Verify retries were enabled + assert mock_configuration.retries == 3 + assert mock_rest_client.retries == 3 + + # Verify RetryClient was created + mock_retry_client.assert_called_once() + + except ImportError: + pytest.skip("Workato class not available due to missing dependencies") diff --git a/uv.lock b/uv.lock index 9e56840..a09bce5 100644 --- a/uv.lock +++ b/uv.lock @@ -1026,11 +1026,11 @@ wheels = [ [[package]] name = "openapi-generator-cli" -version = "7.15.0" +version = "7.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/e7/f62514b564a2ef5ccae3c2270193bf667dfc3d8fcc3b8a443189e31eb313/openapi_generator_cli-7.15.0.tar.gz", hash = "sha256:93b3c1ac6d9d13d1309e95bedcbc0af839dcfe3047c840531cef662b61368fc1", size = 27444822, upload-time = "2025-08-23T01:27:36.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/75/477c36fa8a6d1279c48c4be16155ca9985c6dbc8dd75d3bc1466d81469e9/openapi_generator_cli-7.16.0.tar.gz", hash = "sha256:a056ea34c12b989363c94025a5290ec24b714d80bac2e275bb9366c6bfda130b", size = 27710447, upload-time = "2025-09-29T01:27:50.66Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/ec/5ac2715079da83cbec2f7a1211e46c516e29a1f4a701103ea0010d7c7383/openapi_generator_cli-7.15.0-py3-none-any.whl", hash = "sha256:ff95305ce9a8ad1250fb16d6b0a20e6f5d3041fcb515f4b1a471719dbb1d56ef", size = 27459322, upload-time = "2025-08-23T01:27:33.4Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/c274afa6b0817d6a80704f6ecc5cb98a655e64f5992c14d0813f1288a761/openapi_generator_cli-7.16.0-py3-none-any.whl", hash = "sha256:b4ac990a9c6d3cd7e38a1b914c089449943183fca6fff169cbd219d06123fd7d", size = 27724625, upload-time = "2025-09-29T01:27:47.333Z" }, ] [[package]] @@ -1797,7 +1797,7 @@ dev = [ { name = "build", specifier = ">=1.3.0" }, { name = "hatch-vcs", specifier = ">=0.5.0" }, { name = "mypy", specifier = ">=1.17.1" }, - { name = "openapi-generator-cli", specifier = ">=7.15.0" }, + { name = "openapi-generator-cli", specifier = ">=7.16.0" }, { name = "pip-audit", specifier = ">=2.9.0" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pytest", specifier = ">=7.0.0" }, diff --git a/workato-api-spec.yaml b/workato-api-spec.yaml index 7a0e922..60257ab 100644 --- a/workato-api-spec.yaml +++ b/workato-api-spec.yaml @@ -2454,7 +2454,6 @@ components: type: object required: - name - - title - deprecated - bulk - batch @@ -2464,6 +2463,7 @@ components: example: "new_entry" title: type: string + nullable: true example: "New entry" deprecated: type: boolean