Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions openapi-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ packageVersion: 1.0.0
packageUrl: https://github.com/workato/workato-platform-cli
hideGenerationTimestamp: true
generateSourceCodeOnly: true
lazyImports: false
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/.openapi-generator/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.14.0
7.16.0
45 changes: 45 additions & 0 deletions src/workato_platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from typing import Any

import aiohttp_retry


try:
from workato_platform._version import __version__
Expand All @@ -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."""

Expand All @@ -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)
Expand Down
111 changes: 85 additions & 26 deletions src/workato_platform/cli/commands/recipes/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -581,34 +582,46 @@ 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(
custom_connector.id
)
)
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
Expand Down Expand Up @@ -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"]
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/workato_platform/client/workato_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

13 changes: 8 additions & 5 deletions src/workato_platform/client/workato_api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**name** | **str** | |
**title** | **str** | |
**title** | **str** | | [optional]
**deprecated** | **bool** | |
**bulk** | **bool** | |
**batch** | **bool** | |
Expand Down
2 changes: 1 addition & 1 deletion src/workato_platform/client/workato_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,7 +27,7 @@ class ConnectorAction(BaseModel):
ConnectorAction
""" # noqa: E501
name: StrictStr
title: StrictStr
title: Optional[StrictStr] = None
deprecated: StrictBool
bulk: StrictBool
batch: StrictBool
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/workato_platform/client/workato_api/models/data_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
Loading