autohive_integrations_sdk.integration

  1# Standard Library Imports
  2from abc import ABC, abstractmethod
  3import asyncio
  4from dataclasses import dataclass, field
  5from datetime import timedelta
  6from enum import Enum
  7import json
  8import json as jsonX  # Keep alias to avoid conflict with 'json' parameter in fetch
  9import logging
 10import os
 11from pathlib import Path
 12from typing import Dict, Any, List, Optional, Union, Type, TypeVar, Generic, ClassVar
 13from urllib.parse import urlencode
 14
 15# Third-Party Imports
 16import aiohttp
 17from jsonschema import validate, Draft7Validator
 18
 19# ---- Type Definitions ----
 20T = TypeVar('T')
 21
 22# ---- Auth Types ----
 23class AuthType(Enum):
 24    """The type of authentication to use"""
 25    PlatformOauth2 = "PlatformOauth2"
 26    PlatformTeams = "PlatformTeams"
 27    ApiKey = "ApiKey"
 28    Basic = "Basic"
 29    Custom = "Custom"
 30
 31# ---- Exceptions ----
 32class ValidationError(Exception):
 33    """Raised when inputs/outputs validation fails"""
 34    def __init__(self, message: str, schema: str = None, inputs: str = None):
 35        self.schema = schema
 36        """The schema that failed validation"""
 37        self.inputs = inputs
 38        """The data that failed validation"""
 39        self.message = message
 40        """The error message"""
 41        super().__init__(message)
 42
 43class ConfigurationError(Exception):
 44    """Raised when integration configuration is invalid"""
 45    pass
 46
 47class HTTPError(Exception):
 48    """Custom HTTP error with detailed information"""
 49    def __init__(self, status: int, message: str, response_data: Any = None):
 50        self.status = status
 51        """Status code"""
 52        self.message = message
 53        """Error message"""
 54        self.response_data = response_data
 55        """Response data"""
 56        super().__init__(f"HTTP {status}: {message}")
 57
 58class RateLimitError(HTTPError):
 59    """Raised when rate limited by the API"""
 60    def __init__(self, retry_after: int, *args, **kwargs):
 61        self.retry_after = retry_after
 62        """Retry after"""
 63        super().__init__(*args, **kwargs)
 64
 65# ---- Configuration Classes ----
 66@dataclass
 67class Parameter:
 68    """Definition of a parameter"""
 69    name: str
 70    type: str
 71    description: str
 72    enum: Optional[List[str]] = None
 73    required: bool = True
 74    default: Any = None
 75
 76@dataclass
 77class SchemaDefinition:
 78    """Base class for components that have input/output schemas"""
 79    name: str
 80    description: str
 81    input_schema: List[Parameter]
 82    output_schema: Optional[Dict[str, Any]] = None
 83
 84@dataclass
 85class Action(SchemaDefinition):
 86    """Empty dataclass that inherits from SchemaDefinition"""
 87    pass
 88
 89@dataclass
 90class PollingTrigger(SchemaDefinition):
 91    """Definition of a polling trigger"""
 92    polling_interval: timedelta = field(default_factory=timedelta)
 93
 94@dataclass
 95class IntegrationConfig:
 96    """Configuration for an integration"""
 97    name: str
 98    version: str
 99    description: str
100    auth: Dict[str, Any]
101    actions: Dict[str, Action]
102    polling_triggers: Dict[str, PollingTrigger]
103
104# ---- Base Handler Classes ----
105class ActionHandler(ABC):
106    """Base class for action handlers"""
107    @abstractmethod
108    async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any:
109        """Execute the action"""
110        pass
111
112class PollingTriggerHandler(ABC):
113    """Base class for polling trigger handlers"""
114    @abstractmethod
115    async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]:
116        """Execute the polling trigger"""
117        pass
118
119# ---- Core SDK Classes ----
120class ExecutionContext:
121    """Context provided to integration handlers for making authenticated HTTP requests.
122    
123    This class manages authentication, HTTP sessions, and provides a convenient interface
124    for making API requests with automatic retries, error handling, and logging."""
125    def __init__(
126        self,
127        auth: Dict[str, Any] = {}, 
128        request_config: Optional[Dict[str, Any]] = None,
129        metadata: Optional[Dict[str, Any]] = None,
130        logger: Optional[logging.Logger] = None
131    ):
132        self.auth = auth
133        """Authentication configuration"""
134        self.config = request_config or {"max_retries": 3, "timeout": 30}
135        """Request configuration"""
136        self.metadata = metadata or {}
137        """Additional metadata"""
138        self.logger = logger or logging.getLogger(__name__)
139        """Logger instance"""
140        self._session: Optional[aiohttp.ClientSession] = None
141
142    async def __aenter__(self):
143        if not self._session:
144            self._session = aiohttp.ClientSession()
145        return self
146
147    async def __aexit__(self, exc_type, exc_val, exc_tb):
148        if self._session:
149            await self._session.close()
150            self._session = None
151
152    async def fetch(
153            self,
154            url: str,
155            method: str = "GET",
156            params: Optional[Dict[str, Any]] = None,
157            data: Any = None,
158            json: Any = None,
159            headers: Optional[Dict[str, str]] = None,
160            content_type: Optional[str] = None,
161            timeout: Optional[int] = None,
162            retry_count: int = 0
163    ) -> Any:
164        """Make an authenticated HTTP request.
165        
166        This method handles authentication, retries, error handling, and response parsing.
167        
168        Args:
169            url: The URL to request
170            method: HTTP method to use. Defaults to "GET".
171            params: Query parameters
172            data: Request body data
173            json: JSON data to send (will set content_type to application/json)
174            headers: Additional HTTP headers
175            content_type: Content-Type header
176            timeout: Request timeout in seconds
177            retry_count: Current retry attempt (used internally)
178            
179        Returns:
180            Response data, parsed as JSON if possible
181            
182        Raises:
183            HTTPError: For HTTP error responses
184            RateLimitError: When rate limited by the API
185            Exception: For other request errors
186        """
187        if not self._session:
188            self._session = aiohttp.ClientSession()
189
190        # Prepare request
191        if json is not None:
192            data = json
193            content_type = "application/json"
194
195        final_headers = {}
196        
197        if self.auth and "Authorization" not in (headers or {}):
198            auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2"))
199            credentials = self.auth.get("credentials", {})
200            
201            if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials:
202                final_headers["Authorization"] = f"Bearer {credentials['access_token']}"
203
204        if content_type:
205            final_headers["Content-Type"] = content_type
206        if headers:
207            final_headers.update(headers)
208
209        if params:
210            # Handle nested dictionary parameters
211            flat_params = {}
212            for key, value in params.items():
213                if isinstance(value, (dict, list)):
214                    flat_params[key] = jsonX.dumps(value)
215                elif value is not None:
216                    flat_params[key] = str(value)
217            query_string = urlencode(flat_params)
218            url = f"{url}{'&' if '?' in url else '?'}{query_string}"
219
220        # Prepare body
221        if data is not None:
222            if content_type == "application/json":
223                data = jsonX.dumps(data)
224            elif content_type == "application/x-www-form-urlencoded":
225                data = urlencode(data) if isinstance(data, dict) else data
226
227        # Store the original timeout numeric value
228        original_timeout = timeout or self.config["timeout"]
229
230        # Convert the numeric timeout to a ClientTimeout instance for this request
231        client_timeout = aiohttp.ClientTimeout(total=original_timeout)
232
233        try:
234            async with self._session.request(
235                method=method,
236                url=url,
237                data=data,
238                headers=final_headers,
239                timeout=client_timeout,
240                ssl=True
241            ) as response:
242                content_type = response.headers.get("Content-Type", "")
243
244                if response.status == 429:  # Rate limit
245                    retry_after = int(response.headers.get("Retry-After", 60))
246                    raise RateLimitError(
247                        retry_after,
248                        response.status,
249                        "Rate limit exceeded",
250                        await response.text()
251                    )
252
253                try:
254                    if "application/json" in content_type:
255                        result = await response.json()
256                    else:
257                        result = await response.text()
258                        if not result and response.status in {200, 201, 204}:
259                            return None
260                except Exception as e:
261                    self.logger.error(f"Error parsing response: {e}")
262                    result = await response.text()
263
264                if not response.ok:
265                    print(f"HTTP error encountered. Status: {response.status}. Result: {result}")
266                    raise HTTPError(response.status, str(result), result)
267
268                return result
269
270        except RateLimitError:
271            raise
272        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
273            print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.")
274            if retry_count < self.config["max_retries"]:
275                await asyncio.sleep(2 ** retry_count)  # Exponential backoff
276                print("Retrying request...")
277                # Use original_timeout (numeric) for recursive calls
278                return await self.fetch(
279                    url, method, params, data, json,
280                    headers, content_type, original_timeout, retry_count + 1
281                )
282            else:
283                print("Max retries reached. Raising error.")
284                raise
285        except Exception as e:
286            self.logger.error(f"Unexpected error during {method} {url}: {e}")
287            print(f"Unexpected error encountered: {e}")
288            raise
289
290
291class Integration:
292    """Base integration class with handler registration and execution.
293    
294    This class manages the integration configuration, handler registration,
295    and provides methods to execute actions and triggers.
296    
297    Args:
298        config: Integration configuration
299        
300    Attributes:
301        config: Integration configuration
302    """
303
304    def __init__(self, config: IntegrationConfig):
305        self.config = config
306        """Integration configuration"""
307        self._action_handlers: Dict[str, Type[ActionHandler]] = {}
308        """Action handlers"""
309        self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {}
310        """Polling handlers"""
311
312    @classmethod
313    def load(cls, config_path: Union[str, Path] = None) -> 'Integration':
314        """Load integration from JSON configuration.
315        
316        Args:
317            config_path: Path to the configuration file. Defaults to 'config.json' in the project root.
318            
319        Returns:
320            Initialized integration instance
321            
322        Raises:
323            ConfigurationError: If configuration is invalid or missing
324        """
325        if config_path is None:
326            config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json')
327        
328        config_path = Path(config_path)
329
330        if not config_path.exists():
331            raise ConfigurationError(f"Configuration file not found: {config_path}")
332
333        try:
334            with open(config_path, 'r') as f:
335                config_data = json.load(f)
336        except json.JSONDecodeError as e:
337            raise ConfigurationError(f"Invalid JSON configuration: {e}")
338        
339        # Parse configuration sections
340        actions = cls._parse_actions(config_data.get("actions", {}))
341        polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {}))
342
343        config = IntegrationConfig(
344            name=config_data["name"],
345            version=config_data["version"],
346            description=config_data["description"],
347            auth=config_data.get("auth", {}),
348            actions=actions,
349            polling_triggers=polling_triggers
350        )
351
352        return cls(config)
353
354    @staticmethod
355    def _parse_interval(interval_str: str) -> timedelta:
356        """Parse interval string into timedelta"""
357        unit = interval_str[-1].lower()
358        value = int(interval_str[:-1])
359
360        if unit == 's':
361            return timedelta(seconds=value)
362        elif unit == 'm':
363            return timedelta(minutes=value)
364        elif unit == 'h':
365            return timedelta(hours=value)
366        elif unit == 'd':
367            return timedelta(days=value)
368        else:
369            raise ConfigurationError(f"Invalid interval format: {interval_str}")
370
371    @classmethod
372    def _parse_actions(cls, actions_config: Dict[str, Any]) -> Dict[str, Action]:
373        """Parse action configurations"""
374        actions = {}
375        for name, data in actions_config.items():
376            actions[name] = Action(
377                name=name,
378                description=data["description"],
379                input_schema=data["input_schema"],
380                output_schema=data["output_schema"]
381            )
382
383        return actions
384
385    @classmethod
386    def _parse_polling_triggers(cls, triggers_config: Dict[str, Any]) -> Dict[str, PollingTrigger]:
387        """Parse polling trigger configurations"""
388        triggers = {}
389        for name, data in triggers_config.items():
390            interval = cls._parse_interval(data["polling_interval"])
391
392            triggers[name] = PollingTrigger(
393                name=name,
394                description=data["description"],
395                polling_interval=interval,
396                input_schema=data["input_schema"],
397                output_schema=data["output_schema"]
398            )
399
400        return triggers
401
402    def action(self, name: str):
403        """Decorator to register an action handler.
404        
405        Args:
406            name: Name of the action to register
407            
408        Returns:
409            Decorator function
410            
411        Raises:
412            ConfigurationError: If action is not defined in config
413            
414        Example:
415            ```python
416            @integration.action("my_action")
417            class MyActionHandler(ActionHandler):
418                async def execute(self, inputs, context):
419                    # Implementation
420                    return result
421            ```
422        """
423        def decorator(handler_class: Type[ActionHandler]):
424            if name not in self.config.actions:
425                raise ConfigurationError(f"Action '{name}' not defined in config")
426            self._action_handlers[name] = handler_class
427            return handler_class
428        return decorator
429
430    def polling_trigger(self, name: str):
431        """Decorator to register a polling trigger handler
432        
433        Args:
434            name: Name of the polling trigger to register
435            
436        Returns:
437            Decorator function
438        
439        Raises:
440            ConfigurationError: If polling trigger is not defined in config
441
442        Example:
443            ```python
444            @integration.polling_trigger("my_polling_trigger")
445            class MyPollingTriggerHandler(PollingTriggerHandler):
446                async def poll(self, inputs, last_poll_ts, context):
447                    # Implementation
448                    return result
449            ```
450        """
451        def decorator(handler_class: Type[PollingTriggerHandler]):
452            if name not in self.config.polling_triggers:
453                raise ConfigurationError(f"Polling trigger '{name}' not defined in config")
454            self._polling_handlers[name] = handler_class
455            return handler_class
456        return decorator
457
458    async def execute_action(self,
459                           name: str,
460                           inputs: Dict[str, Any],
461                           context: ExecutionContext) -> Any:
462        """Execute a registered action.
463        
464        Args:
465            name: Name of the action to execute
466            inputs: Action inputs
467            context: Execution context
468            
469        Returns:
470            Action result
471            
472        Raises:
473            ValidationError: If inputs or outputs don't match schema
474        """
475        if name not in self._action_handlers:
476            raise ValidationError(f"Action '{name}' not registered")
477
478        # Validate inputs against action schema
479        action_config = self.config.actions[name]
480        validator = Draft7Validator(action_config.input_schema)
481        errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path)
482        if errors:
483            message = ""
484            for error in errors:
485                message += f"{list(error.schema_path)}, {error.message},\n "
486            raise ValidationError(message, action_config.input_schema, inputs)
487         
488        if "fields" in self.config.auth:
489            auth_config = self.config.auth["fields"]
490            validator = Draft7Validator(auth_config)
491            errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path)
492            if errors:
493                message = ""
494                for error in errors:
495                    message += f"{list(error.schema_path)}, {error.message},\n "
496                raise ValidationError(message, auth_config, context.auth)
497
498        # Create handler instance and execute
499        handler = self._action_handlers[name]()
500        result = await handler.execute(inputs, context)
501
502        # Validate output if schema is defined
503        validator = Draft7Validator(action_config.output_schema)
504        errors = sorted(validator.iter_errors(result), key=lambda e: e.path)
505        if errors:
506            message = ""
507            for error in errors:
508                message += f"{list(error.schema_path)}, {error.message},\n "
509            raise ValidationError(message, action_config.output_schema, result)
510     
511        return result
512
513    async def execute_polling_trigger(self,
514                                    name: str,
515                                    inputs: Dict[str, Any],
516                                    last_poll_ts: Optional[str],
517                                    context: ExecutionContext) -> List[Dict[str, Any]]:
518        """Execute a registered polling trigger
519        
520        Args:
521            name: Name of the polling trigger to execute
522            inputs: Trigger inputs
523            last_poll_ts: Last poll timestamp
524            context: Execution context
525            
526        Returns:
527            List of records
528            
529        Raises:
530            ValidationError: If inputs or outputs don't match schema
531        """
532        if name not in self._polling_handlers:
533            raise ValidationError(f"Polling trigger '{name}' not registered")
534
535        # Validate trigger configuration
536        trigger_config = self.config.polling_triggers[name]
537        try:
538            validate(inputs, trigger_config.input_schema)
539        except Exception as e:
540            raise ValidationError(e.message, e.schema, e.instance)
541
542        try:
543            auth_config = self.config.auth["fields"]
544            validate(context.auth, auth_config)
545        except Exception as e:
546            raise ValidationError(e.message, e.schema, e.instance)
547        
548        # Create handler instance and execute
549        handler = self._polling_handlers[name]()
550        records = await handler.poll(inputs, last_poll_ts, context)
551        # Validate each record
552        for record in records:
553            if "id" not in record:
554                raise ValidationError(
555                    f"Polling trigger '{name}' returned record without required 'id' field")
556            if "data" not in record:
557                raise ValidationError(
558                    f"Polling trigger '{name}' returned record without required 'data' field")
559
560            # Validate record data against output schema
561            try:
562                validate(record["data"], trigger_config.output_schema)
563            except Exception as e:
564                raise ValidationError(e.message, e.schema, e.instance)
565            
566        return records
class AuthType(enum.Enum):
24class AuthType(Enum):
25    """The type of authentication to use"""
26    PlatformOauth2 = "PlatformOauth2"
27    PlatformTeams = "PlatformTeams"
28    ApiKey = "ApiKey"
29    Basic = "Basic"
30    Custom = "Custom"

The type of authentication to use

PlatformOauth2 = <AuthType.PlatformOauth2: 'PlatformOauth2'>
PlatformTeams = <AuthType.PlatformTeams: 'PlatformTeams'>
ApiKey = <AuthType.ApiKey: 'ApiKey'>
Basic = <AuthType.Basic: 'Basic'>
Custom = <AuthType.Custom: 'Custom'>
class ValidationError(builtins.Exception):
33class ValidationError(Exception):
34    """Raised when inputs/outputs validation fails"""
35    def __init__(self, message: str, schema: str = None, inputs: str = None):
36        self.schema = schema
37        """The schema that failed validation"""
38        self.inputs = inputs
39        """The data that failed validation"""
40        self.message = message
41        """The error message"""
42        super().__init__(message)

Raised when inputs/outputs validation fails

ValidationError(message: str, schema: str = None, inputs: str = None)
35    def __init__(self, message: str, schema: str = None, inputs: str = None):
36        self.schema = schema
37        """The schema that failed validation"""
38        self.inputs = inputs
39        """The data that failed validation"""
40        self.message = message
41        """The error message"""
42        super().__init__(message)
schema

The schema that failed validation

inputs

The data that failed validation

message

The error message

class ConfigurationError(builtins.Exception):
44class ConfigurationError(Exception):
45    """Raised when integration configuration is invalid"""
46    pass

Raised when integration configuration is invalid

class HTTPError(builtins.Exception):
48class HTTPError(Exception):
49    """Custom HTTP error with detailed information"""
50    def __init__(self, status: int, message: str, response_data: Any = None):
51        self.status = status
52        """Status code"""
53        self.message = message
54        """Error message"""
55        self.response_data = response_data
56        """Response data"""
57        super().__init__(f"HTTP {status}: {message}")

Custom HTTP error with detailed information

HTTPError(status: int, message: str, response_data: Any = None)
50    def __init__(self, status: int, message: str, response_data: Any = None):
51        self.status = status
52        """Status code"""
53        self.message = message
54        """Error message"""
55        self.response_data = response_data
56        """Response data"""
57        super().__init__(f"HTTP {status}: {message}")
status

Status code

message

Error message

response_data

Response data

class RateLimitError(HTTPError):
59class RateLimitError(HTTPError):
60    """Raised when rate limited by the API"""
61    def __init__(self, retry_after: int, *args, **kwargs):
62        self.retry_after = retry_after
63        """Retry after"""
64        super().__init__(*args, **kwargs)

Raised when rate limited by the API

RateLimitError(retry_after: int, *args, **kwargs)
61    def __init__(self, retry_after: int, *args, **kwargs):
62        self.retry_after = retry_after
63        """Retry after"""
64        super().__init__(*args, **kwargs)
retry_after

Retry after

@dataclass
class Parameter:
67@dataclass
68class Parameter:
69    """Definition of a parameter"""
70    name: str
71    type: str
72    description: str
73    enum: Optional[List[str]] = None
74    required: bool = True
75    default: Any = None

Definition of a parameter

Parameter( name: str, type: str, description: str, enum: Optional[List[str]] = None, required: bool = True, default: Any = None)
name: str
type: str
description: str
enum: Optional[List[str]] = None
required: bool = True
default: Any = None
@dataclass
class SchemaDefinition:
77@dataclass
78class SchemaDefinition:
79    """Base class for components that have input/output schemas"""
80    name: str
81    description: str
82    input_schema: List[Parameter]
83    output_schema: Optional[Dict[str, Any]] = None

Base class for components that have input/output schemas

SchemaDefinition( name: str, description: str, input_schema: List[Parameter], output_schema: Optional[Dict[str, Any]] = None)
name: str
description: str
input_schema: List[Parameter]
output_schema: Optional[Dict[str, Any]] = None
@dataclass
class Action(SchemaDefinition):
85@dataclass
86class Action(SchemaDefinition):
87    """Empty dataclass that inherits from SchemaDefinition"""
88    pass

Empty dataclass that inherits from SchemaDefinition

Action( name: str, description: str, input_schema: List[Parameter], output_schema: Optional[Dict[str, Any]] = None)
@dataclass
class PollingTrigger(SchemaDefinition):
90@dataclass
91class PollingTrigger(SchemaDefinition):
92    """Definition of a polling trigger"""
93    polling_interval: timedelta = field(default_factory=timedelta)

Definition of a polling trigger

PollingTrigger( name: str, description: str, input_schema: List[Parameter], output_schema: Optional[Dict[str, Any]] = None, polling_interval: datetime.timedelta = <factory>)
polling_interval: datetime.timedelta
@dataclass
class IntegrationConfig:
 95@dataclass
 96class IntegrationConfig:
 97    """Configuration for an integration"""
 98    name: str
 99    version: str
100    description: str
101    auth: Dict[str, Any]
102    actions: Dict[str, Action]
103    polling_triggers: Dict[str, PollingTrigger]

Configuration for an integration

IntegrationConfig( name: str, version: str, description: str, auth: Dict[str, Any], actions: Dict[str, Action], polling_triggers: Dict[str, PollingTrigger])
name: str
version: str
description: str
auth: Dict[str, Any]
actions: Dict[str, Action]
polling_triggers: Dict[str, PollingTrigger]
class ActionHandler(abc.ABC):
106class ActionHandler(ABC):
107    """Base class for action handlers"""
108    @abstractmethod
109    async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any:
110        """Execute the action"""
111        pass

Base class for action handlers

@abstractmethod
async def execute( self, inputs: Dict[str, Any], context: ExecutionContext) -> Any:
108    @abstractmethod
109    async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any:
110        """Execute the action"""
111        pass

Execute the action

class PollingTriggerHandler(abc.ABC):
113class PollingTriggerHandler(ABC):
114    """Base class for polling trigger handlers"""
115    @abstractmethod
116    async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]:
117        """Execute the polling trigger"""
118        pass

Base class for polling trigger handlers

@abstractmethod
async def poll( self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: ExecutionContext) -> List[Dict[str, Any]]:
115    @abstractmethod
116    async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]:
117        """Execute the polling trigger"""
118        pass

Execute the polling trigger

class ExecutionContext:
121class ExecutionContext:
122    """Context provided to integration handlers for making authenticated HTTP requests.
123    
124    This class manages authentication, HTTP sessions, and provides a convenient interface
125    for making API requests with automatic retries, error handling, and logging."""
126    def __init__(
127        self,
128        auth: Dict[str, Any] = {}, 
129        request_config: Optional[Dict[str, Any]] = None,
130        metadata: Optional[Dict[str, Any]] = None,
131        logger: Optional[logging.Logger] = None
132    ):
133        self.auth = auth
134        """Authentication configuration"""
135        self.config = request_config or {"max_retries": 3, "timeout": 30}
136        """Request configuration"""
137        self.metadata = metadata or {}
138        """Additional metadata"""
139        self.logger = logger or logging.getLogger(__name__)
140        """Logger instance"""
141        self._session: Optional[aiohttp.ClientSession] = None
142
143    async def __aenter__(self):
144        if not self._session:
145            self._session = aiohttp.ClientSession()
146        return self
147
148    async def __aexit__(self, exc_type, exc_val, exc_tb):
149        if self._session:
150            await self._session.close()
151            self._session = None
152
153    async def fetch(
154            self,
155            url: str,
156            method: str = "GET",
157            params: Optional[Dict[str, Any]] = None,
158            data: Any = None,
159            json: Any = None,
160            headers: Optional[Dict[str, str]] = None,
161            content_type: Optional[str] = None,
162            timeout: Optional[int] = None,
163            retry_count: int = 0
164    ) -> Any:
165        """Make an authenticated HTTP request.
166        
167        This method handles authentication, retries, error handling, and response parsing.
168        
169        Args:
170            url: The URL to request
171            method: HTTP method to use. Defaults to "GET".
172            params: Query parameters
173            data: Request body data
174            json: JSON data to send (will set content_type to application/json)
175            headers: Additional HTTP headers
176            content_type: Content-Type header
177            timeout: Request timeout in seconds
178            retry_count: Current retry attempt (used internally)
179            
180        Returns:
181            Response data, parsed as JSON if possible
182            
183        Raises:
184            HTTPError: For HTTP error responses
185            RateLimitError: When rate limited by the API
186            Exception: For other request errors
187        """
188        if not self._session:
189            self._session = aiohttp.ClientSession()
190
191        # Prepare request
192        if json is not None:
193            data = json
194            content_type = "application/json"
195
196        final_headers = {}
197        
198        if self.auth and "Authorization" not in (headers or {}):
199            auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2"))
200            credentials = self.auth.get("credentials", {})
201            
202            if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials:
203                final_headers["Authorization"] = f"Bearer {credentials['access_token']}"
204
205        if content_type:
206            final_headers["Content-Type"] = content_type
207        if headers:
208            final_headers.update(headers)
209
210        if params:
211            # Handle nested dictionary parameters
212            flat_params = {}
213            for key, value in params.items():
214                if isinstance(value, (dict, list)):
215                    flat_params[key] = jsonX.dumps(value)
216                elif value is not None:
217                    flat_params[key] = str(value)
218            query_string = urlencode(flat_params)
219            url = f"{url}{'&' if '?' in url else '?'}{query_string}"
220
221        # Prepare body
222        if data is not None:
223            if content_type == "application/json":
224                data = jsonX.dumps(data)
225            elif content_type == "application/x-www-form-urlencoded":
226                data = urlencode(data) if isinstance(data, dict) else data
227
228        # Store the original timeout numeric value
229        original_timeout = timeout or self.config["timeout"]
230
231        # Convert the numeric timeout to a ClientTimeout instance for this request
232        client_timeout = aiohttp.ClientTimeout(total=original_timeout)
233
234        try:
235            async with self._session.request(
236                method=method,
237                url=url,
238                data=data,
239                headers=final_headers,
240                timeout=client_timeout,
241                ssl=True
242            ) as response:
243                content_type = response.headers.get("Content-Type", "")
244
245                if response.status == 429:  # Rate limit
246                    retry_after = int(response.headers.get("Retry-After", 60))
247                    raise RateLimitError(
248                        retry_after,
249                        response.status,
250                        "Rate limit exceeded",
251                        await response.text()
252                    )
253
254                try:
255                    if "application/json" in content_type:
256                        result = await response.json()
257                    else:
258                        result = await response.text()
259                        if not result and response.status in {200, 201, 204}:
260                            return None
261                except Exception as e:
262                    self.logger.error(f"Error parsing response: {e}")
263                    result = await response.text()
264
265                if not response.ok:
266                    print(f"HTTP error encountered. Status: {response.status}. Result: {result}")
267                    raise HTTPError(response.status, str(result), result)
268
269                return result
270
271        except RateLimitError:
272            raise
273        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
274            print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.")
275            if retry_count < self.config["max_retries"]:
276                await asyncio.sleep(2 ** retry_count)  # Exponential backoff
277                print("Retrying request...")
278                # Use original_timeout (numeric) for recursive calls
279                return await self.fetch(
280                    url, method, params, data, json,
281                    headers, content_type, original_timeout, retry_count + 1
282                )
283            else:
284                print("Max retries reached. Raising error.")
285                raise
286        except Exception as e:
287            self.logger.error(f"Unexpected error during {method} {url}: {e}")
288            print(f"Unexpected error encountered: {e}")
289            raise

Context provided to integration handlers for making authenticated HTTP requests.

This class manages authentication, HTTP sessions, and provides a convenient interface for making API requests with automatic retries, error handling, and logging.

ExecutionContext( auth: Dict[str, Any] = {}, request_config: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None, logger: Optional[logging.Logger] = None)
126    def __init__(
127        self,
128        auth: Dict[str, Any] = {}, 
129        request_config: Optional[Dict[str, Any]] = None,
130        metadata: Optional[Dict[str, Any]] = None,
131        logger: Optional[logging.Logger] = None
132    ):
133        self.auth = auth
134        """Authentication configuration"""
135        self.config = request_config or {"max_retries": 3, "timeout": 30}
136        """Request configuration"""
137        self.metadata = metadata or {}
138        """Additional metadata"""
139        self.logger = logger or logging.getLogger(__name__)
140        """Logger instance"""
141        self._session: Optional[aiohttp.ClientSession] = None
auth

Authentication configuration

config

Request configuration

metadata

Additional metadata

logger

Logger instance

async def fetch( self, url: str, method: str = 'GET', params: Optional[Dict[str, Any]] = None, data: Any = None, json: Any = None, headers: Optional[Dict[str, str]] = None, content_type: Optional[str] = None, timeout: Optional[int] = None, retry_count: int = 0) -> Any:
153    async def fetch(
154            self,
155            url: str,
156            method: str = "GET",
157            params: Optional[Dict[str, Any]] = None,
158            data: Any = None,
159            json: Any = None,
160            headers: Optional[Dict[str, str]] = None,
161            content_type: Optional[str] = None,
162            timeout: Optional[int] = None,
163            retry_count: int = 0
164    ) -> Any:
165        """Make an authenticated HTTP request.
166        
167        This method handles authentication, retries, error handling, and response parsing.
168        
169        Args:
170            url: The URL to request
171            method: HTTP method to use. Defaults to "GET".
172            params: Query parameters
173            data: Request body data
174            json: JSON data to send (will set content_type to application/json)
175            headers: Additional HTTP headers
176            content_type: Content-Type header
177            timeout: Request timeout in seconds
178            retry_count: Current retry attempt (used internally)
179            
180        Returns:
181            Response data, parsed as JSON if possible
182            
183        Raises:
184            HTTPError: For HTTP error responses
185            RateLimitError: When rate limited by the API
186            Exception: For other request errors
187        """
188        if not self._session:
189            self._session = aiohttp.ClientSession()
190
191        # Prepare request
192        if json is not None:
193            data = json
194            content_type = "application/json"
195
196        final_headers = {}
197        
198        if self.auth and "Authorization" not in (headers or {}):
199            auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2"))
200            credentials = self.auth.get("credentials", {})
201            
202            if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials:
203                final_headers["Authorization"] = f"Bearer {credentials['access_token']}"
204
205        if content_type:
206            final_headers["Content-Type"] = content_type
207        if headers:
208            final_headers.update(headers)
209
210        if params:
211            # Handle nested dictionary parameters
212            flat_params = {}
213            for key, value in params.items():
214                if isinstance(value, (dict, list)):
215                    flat_params[key] = jsonX.dumps(value)
216                elif value is not None:
217                    flat_params[key] = str(value)
218            query_string = urlencode(flat_params)
219            url = f"{url}{'&' if '?' in url else '?'}{query_string}"
220
221        # Prepare body
222        if data is not None:
223            if content_type == "application/json":
224                data = jsonX.dumps(data)
225            elif content_type == "application/x-www-form-urlencoded":
226                data = urlencode(data) if isinstance(data, dict) else data
227
228        # Store the original timeout numeric value
229        original_timeout = timeout or self.config["timeout"]
230
231        # Convert the numeric timeout to a ClientTimeout instance for this request
232        client_timeout = aiohttp.ClientTimeout(total=original_timeout)
233
234        try:
235            async with self._session.request(
236                method=method,
237                url=url,
238                data=data,
239                headers=final_headers,
240                timeout=client_timeout,
241                ssl=True
242            ) as response:
243                content_type = response.headers.get("Content-Type", "")
244
245                if response.status == 429:  # Rate limit
246                    retry_after = int(response.headers.get("Retry-After", 60))
247                    raise RateLimitError(
248                        retry_after,
249                        response.status,
250                        "Rate limit exceeded",
251                        await response.text()
252                    )
253
254                try:
255                    if "application/json" in content_type:
256                        result = await response.json()
257                    else:
258                        result = await response.text()
259                        if not result and response.status in {200, 201, 204}:
260                            return None
261                except Exception as e:
262                    self.logger.error(f"Error parsing response: {e}")
263                    result = await response.text()
264
265                if not response.ok:
266                    print(f"HTTP error encountered. Status: {response.status}. Result: {result}")
267                    raise HTTPError(response.status, str(result), result)
268
269                return result
270
271        except RateLimitError:
272            raise
273        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
274            print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.")
275            if retry_count < self.config["max_retries"]:
276                await asyncio.sleep(2 ** retry_count)  # Exponential backoff
277                print("Retrying request...")
278                # Use original_timeout (numeric) for recursive calls
279                return await self.fetch(
280                    url, method, params, data, json,
281                    headers, content_type, original_timeout, retry_count + 1
282                )
283            else:
284                print("Max retries reached. Raising error.")
285                raise
286        except Exception as e:
287            self.logger.error(f"Unexpected error during {method} {url}: {e}")
288            print(f"Unexpected error encountered: {e}")
289            raise

Make an authenticated HTTP request.

This method handles authentication, retries, error handling, and response parsing.

Arguments:
  • url: The URL to request
  • method: HTTP method to use. Defaults to "GET".
  • params: Query parameters
  • data: Request body data
  • json: JSON data to send (will set content_type to application/json)
  • headers: Additional HTTP headers
  • content_type: Content-Type header
  • timeout: Request timeout in seconds
  • retry_count: Current retry attempt (used internally)
Returns:

Response data, parsed as JSON if possible

Raises:
  • HTTPError: For HTTP error responses
  • RateLimitError: When rate limited by the API
  • Exception: For other request errors
class Integration:
292class Integration:
293    """Base integration class with handler registration and execution.
294    
295    This class manages the integration configuration, handler registration,
296    and provides methods to execute actions and triggers.
297    
298    Args:
299        config: Integration configuration
300        
301    Attributes:
302        config: Integration configuration
303    """
304
305    def __init__(self, config: IntegrationConfig):
306        self.config = config
307        """Integration configuration"""
308        self._action_handlers: Dict[str, Type[ActionHandler]] = {}
309        """Action handlers"""
310        self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {}
311        """Polling handlers"""
312
313    @classmethod
314    def load(cls, config_path: Union[str, Path] = None) -> 'Integration':
315        """Load integration from JSON configuration.
316        
317        Args:
318            config_path: Path to the configuration file. Defaults to 'config.json' in the project root.
319            
320        Returns:
321            Initialized integration instance
322            
323        Raises:
324            ConfigurationError: If configuration is invalid or missing
325        """
326        if config_path is None:
327            config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json')
328        
329        config_path = Path(config_path)
330
331        if not config_path.exists():
332            raise ConfigurationError(f"Configuration file not found: {config_path}")
333
334        try:
335            with open(config_path, 'r') as f:
336                config_data = json.load(f)
337        except json.JSONDecodeError as e:
338            raise ConfigurationError(f"Invalid JSON configuration: {e}")
339        
340        # Parse configuration sections
341        actions = cls._parse_actions(config_data.get("actions", {}))
342        polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {}))
343
344        config = IntegrationConfig(
345            name=config_data["name"],
346            version=config_data["version"],
347            description=config_data["description"],
348            auth=config_data.get("auth", {}),
349            actions=actions,
350            polling_triggers=polling_triggers
351        )
352
353        return cls(config)
354
355    @staticmethod
356    def _parse_interval(interval_str: str) -> timedelta:
357        """Parse interval string into timedelta"""
358        unit = interval_str[-1].lower()
359        value = int(interval_str[:-1])
360
361        if unit == 's':
362            return timedelta(seconds=value)
363        elif unit == 'm':
364            return timedelta(minutes=value)
365        elif unit == 'h':
366            return timedelta(hours=value)
367        elif unit == 'd':
368            return timedelta(days=value)
369        else:
370            raise ConfigurationError(f"Invalid interval format: {interval_str}")
371
372    @classmethod
373    def _parse_actions(cls, actions_config: Dict[str, Any]) -> Dict[str, Action]:
374        """Parse action configurations"""
375        actions = {}
376        for name, data in actions_config.items():
377            actions[name] = Action(
378                name=name,
379                description=data["description"],
380                input_schema=data["input_schema"],
381                output_schema=data["output_schema"]
382            )
383
384        return actions
385
386    @classmethod
387    def _parse_polling_triggers(cls, triggers_config: Dict[str, Any]) -> Dict[str, PollingTrigger]:
388        """Parse polling trigger configurations"""
389        triggers = {}
390        for name, data in triggers_config.items():
391            interval = cls._parse_interval(data["polling_interval"])
392
393            triggers[name] = PollingTrigger(
394                name=name,
395                description=data["description"],
396                polling_interval=interval,
397                input_schema=data["input_schema"],
398                output_schema=data["output_schema"]
399            )
400
401        return triggers
402
403    def action(self, name: str):
404        """Decorator to register an action handler.
405        
406        Args:
407            name: Name of the action to register
408            
409        Returns:
410            Decorator function
411            
412        Raises:
413            ConfigurationError: If action is not defined in config
414            
415        Example:
416            ```python
417            @integration.action("my_action")
418            class MyActionHandler(ActionHandler):
419                async def execute(self, inputs, context):
420                    # Implementation
421                    return result
422            ```
423        """
424        def decorator(handler_class: Type[ActionHandler]):
425            if name not in self.config.actions:
426                raise ConfigurationError(f"Action '{name}' not defined in config")
427            self._action_handlers[name] = handler_class
428            return handler_class
429        return decorator
430
431    def polling_trigger(self, name: str):
432        """Decorator to register a polling trigger handler
433        
434        Args:
435            name: Name of the polling trigger to register
436            
437        Returns:
438            Decorator function
439        
440        Raises:
441            ConfigurationError: If polling trigger is not defined in config
442
443        Example:
444            ```python
445            @integration.polling_trigger("my_polling_trigger")
446            class MyPollingTriggerHandler(PollingTriggerHandler):
447                async def poll(self, inputs, last_poll_ts, context):
448                    # Implementation
449                    return result
450            ```
451        """
452        def decorator(handler_class: Type[PollingTriggerHandler]):
453            if name not in self.config.polling_triggers:
454                raise ConfigurationError(f"Polling trigger '{name}' not defined in config")
455            self._polling_handlers[name] = handler_class
456            return handler_class
457        return decorator
458
459    async def execute_action(self,
460                           name: str,
461                           inputs: Dict[str, Any],
462                           context: ExecutionContext) -> Any:
463        """Execute a registered action.
464        
465        Args:
466            name: Name of the action to execute
467            inputs: Action inputs
468            context: Execution context
469            
470        Returns:
471            Action result
472            
473        Raises:
474            ValidationError: If inputs or outputs don't match schema
475        """
476        if name not in self._action_handlers:
477            raise ValidationError(f"Action '{name}' not registered")
478
479        # Validate inputs against action schema
480        action_config = self.config.actions[name]
481        validator = Draft7Validator(action_config.input_schema)
482        errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path)
483        if errors:
484            message = ""
485            for error in errors:
486                message += f"{list(error.schema_path)}, {error.message},\n "
487            raise ValidationError(message, action_config.input_schema, inputs)
488         
489        if "fields" in self.config.auth:
490            auth_config = self.config.auth["fields"]
491            validator = Draft7Validator(auth_config)
492            errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path)
493            if errors:
494                message = ""
495                for error in errors:
496                    message += f"{list(error.schema_path)}, {error.message},\n "
497                raise ValidationError(message, auth_config, context.auth)
498
499        # Create handler instance and execute
500        handler = self._action_handlers[name]()
501        result = await handler.execute(inputs, context)
502
503        # Validate output if schema is defined
504        validator = Draft7Validator(action_config.output_schema)
505        errors = sorted(validator.iter_errors(result), key=lambda e: e.path)
506        if errors:
507            message = ""
508            for error in errors:
509                message += f"{list(error.schema_path)}, {error.message},\n "
510            raise ValidationError(message, action_config.output_schema, result)
511     
512        return result
513
514    async def execute_polling_trigger(self,
515                                    name: str,
516                                    inputs: Dict[str, Any],
517                                    last_poll_ts: Optional[str],
518                                    context: ExecutionContext) -> List[Dict[str, Any]]:
519        """Execute a registered polling trigger
520        
521        Args:
522            name: Name of the polling trigger to execute
523            inputs: Trigger inputs
524            last_poll_ts: Last poll timestamp
525            context: Execution context
526            
527        Returns:
528            List of records
529            
530        Raises:
531            ValidationError: If inputs or outputs don't match schema
532        """
533        if name not in self._polling_handlers:
534            raise ValidationError(f"Polling trigger '{name}' not registered")
535
536        # Validate trigger configuration
537        trigger_config = self.config.polling_triggers[name]
538        try:
539            validate(inputs, trigger_config.input_schema)
540        except Exception as e:
541            raise ValidationError(e.message, e.schema, e.instance)
542
543        try:
544            auth_config = self.config.auth["fields"]
545            validate(context.auth, auth_config)
546        except Exception as e:
547            raise ValidationError(e.message, e.schema, e.instance)
548        
549        # Create handler instance and execute
550        handler = self._polling_handlers[name]()
551        records = await handler.poll(inputs, last_poll_ts, context)
552        # Validate each record
553        for record in records:
554            if "id" not in record:
555                raise ValidationError(
556                    f"Polling trigger '{name}' returned record without required 'id' field")
557            if "data" not in record:
558                raise ValidationError(
559                    f"Polling trigger '{name}' returned record without required 'data' field")
560
561            # Validate record data against output schema
562            try:
563                validate(record["data"], trigger_config.output_schema)
564            except Exception as e:
565                raise ValidationError(e.message, e.schema, e.instance)
566            
567        return records

Base integration class with handler registration and execution.

This class manages the integration configuration, handler registration, and provides methods to execute actions and triggers.

Arguments:
  • config: Integration configuration
Attributes:
  • config: Integration configuration
Integration(config: IntegrationConfig)
305    def __init__(self, config: IntegrationConfig):
306        self.config = config
307        """Integration configuration"""
308        self._action_handlers: Dict[str, Type[ActionHandler]] = {}
309        """Action handlers"""
310        self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {}
311        """Polling handlers"""
config

Integration configuration

@classmethod
def load( cls, config_path: Union[str, pathlib.Path] = None) -> Integration:
313    @classmethod
314    def load(cls, config_path: Union[str, Path] = None) -> 'Integration':
315        """Load integration from JSON configuration.
316        
317        Args:
318            config_path: Path to the configuration file. Defaults to 'config.json' in the project root.
319            
320        Returns:
321            Initialized integration instance
322            
323        Raises:
324            ConfigurationError: If configuration is invalid or missing
325        """
326        if config_path is None:
327            config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json')
328        
329        config_path = Path(config_path)
330
331        if not config_path.exists():
332            raise ConfigurationError(f"Configuration file not found: {config_path}")
333
334        try:
335            with open(config_path, 'r') as f:
336                config_data = json.load(f)
337        except json.JSONDecodeError as e:
338            raise ConfigurationError(f"Invalid JSON configuration: {e}")
339        
340        # Parse configuration sections
341        actions = cls._parse_actions(config_data.get("actions", {}))
342        polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {}))
343
344        config = IntegrationConfig(
345            name=config_data["name"],
346            version=config_data["version"],
347            description=config_data["description"],
348            auth=config_data.get("auth", {}),
349            actions=actions,
350            polling_triggers=polling_triggers
351        )
352
353        return cls(config)

Load integration from JSON configuration.

Arguments:
  • config_path: Path to the configuration file. Defaults to 'config.json' in the project root.
Returns:

Initialized integration instance

Raises:
  • ConfigurationError: If configuration is invalid or missing
def action(self, name: str):
403    def action(self, name: str):
404        """Decorator to register an action handler.
405        
406        Args:
407            name: Name of the action to register
408            
409        Returns:
410            Decorator function
411            
412        Raises:
413            ConfigurationError: If action is not defined in config
414            
415        Example:
416            ```python
417            @integration.action("my_action")
418            class MyActionHandler(ActionHandler):
419                async def execute(self, inputs, context):
420                    # Implementation
421                    return result
422            ```
423        """
424        def decorator(handler_class: Type[ActionHandler]):
425            if name not in self.config.actions:
426                raise ConfigurationError(f"Action '{name}' not defined in config")
427            self._action_handlers[name] = handler_class
428            return handler_class
429        return decorator

Decorator to register an action handler.

Arguments:
  • name: Name of the action to register
Returns:

Decorator function

Raises:
  • ConfigurationError: If action is not defined in config
Example:
@integration.action("my_action")
class MyActionHandler(ActionHandler):
    async def execute(self, inputs, context):
        # Implementation
        return result
def polling_trigger(self, name: str):
431    def polling_trigger(self, name: str):
432        """Decorator to register a polling trigger handler
433        
434        Args:
435            name: Name of the polling trigger to register
436            
437        Returns:
438            Decorator function
439        
440        Raises:
441            ConfigurationError: If polling trigger is not defined in config
442
443        Example:
444            ```python
445            @integration.polling_trigger("my_polling_trigger")
446            class MyPollingTriggerHandler(PollingTriggerHandler):
447                async def poll(self, inputs, last_poll_ts, context):
448                    # Implementation
449                    return result
450            ```
451        """
452        def decorator(handler_class: Type[PollingTriggerHandler]):
453            if name not in self.config.polling_triggers:
454                raise ConfigurationError(f"Polling trigger '{name}' not defined in config")
455            self._polling_handlers[name] = handler_class
456            return handler_class
457        return decorator

Decorator to register a polling trigger handler

Arguments:
  • name: Name of the polling trigger to register
Returns:

Decorator function

Raises:
  • ConfigurationError: If polling trigger is not defined in config
Example:
@integration.polling_trigger("my_polling_trigger")
class MyPollingTriggerHandler(PollingTriggerHandler):
    async def poll(self, inputs, last_poll_ts, context):
        # Implementation
        return result
async def execute_action( self, name: str, inputs: Dict[str, Any], context: ExecutionContext) -> Any:
459    async def execute_action(self,
460                           name: str,
461                           inputs: Dict[str, Any],
462                           context: ExecutionContext) -> Any:
463        """Execute a registered action.
464        
465        Args:
466            name: Name of the action to execute
467            inputs: Action inputs
468            context: Execution context
469            
470        Returns:
471            Action result
472            
473        Raises:
474            ValidationError: If inputs or outputs don't match schema
475        """
476        if name not in self._action_handlers:
477            raise ValidationError(f"Action '{name}' not registered")
478
479        # Validate inputs against action schema
480        action_config = self.config.actions[name]
481        validator = Draft7Validator(action_config.input_schema)
482        errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path)
483        if errors:
484            message = ""
485            for error in errors:
486                message += f"{list(error.schema_path)}, {error.message},\n "
487            raise ValidationError(message, action_config.input_schema, inputs)
488         
489        if "fields" in self.config.auth:
490            auth_config = self.config.auth["fields"]
491            validator = Draft7Validator(auth_config)
492            errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path)
493            if errors:
494                message = ""
495                for error in errors:
496                    message += f"{list(error.schema_path)}, {error.message},\n "
497                raise ValidationError(message, auth_config, context.auth)
498
499        # Create handler instance and execute
500        handler = self._action_handlers[name]()
501        result = await handler.execute(inputs, context)
502
503        # Validate output if schema is defined
504        validator = Draft7Validator(action_config.output_schema)
505        errors = sorted(validator.iter_errors(result), key=lambda e: e.path)
506        if errors:
507            message = ""
508            for error in errors:
509                message += f"{list(error.schema_path)}, {error.message},\n "
510            raise ValidationError(message, action_config.output_schema, result)
511     
512        return result

Execute a registered action.

Arguments:
  • name: Name of the action to execute
  • inputs: Action inputs
  • context: Execution context
Returns:

Action result

Raises:
  • ValidationError: If inputs or outputs don't match schema
async def execute_polling_trigger( self, name: str, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: ExecutionContext) -> List[Dict[str, Any]]:
514    async def execute_polling_trigger(self,
515                                    name: str,
516                                    inputs: Dict[str, Any],
517                                    last_poll_ts: Optional[str],
518                                    context: ExecutionContext) -> List[Dict[str, Any]]:
519        """Execute a registered polling trigger
520        
521        Args:
522            name: Name of the polling trigger to execute
523            inputs: Trigger inputs
524            last_poll_ts: Last poll timestamp
525            context: Execution context
526            
527        Returns:
528            List of records
529            
530        Raises:
531            ValidationError: If inputs or outputs don't match schema
532        """
533        if name not in self._polling_handlers:
534            raise ValidationError(f"Polling trigger '{name}' not registered")
535
536        # Validate trigger configuration
537        trigger_config = self.config.polling_triggers[name]
538        try:
539            validate(inputs, trigger_config.input_schema)
540        except Exception as e:
541            raise ValidationError(e.message, e.schema, e.instance)
542
543        try:
544            auth_config = self.config.auth["fields"]
545            validate(context.auth, auth_config)
546        except Exception as e:
547            raise ValidationError(e.message, e.schema, e.instance)
548        
549        # Create handler instance and execute
550        handler = self._polling_handlers[name]()
551        records = await handler.poll(inputs, last_poll_ts, context)
552        # Validate each record
553        for record in records:
554            if "id" not in record:
555                raise ValidationError(
556                    f"Polling trigger '{name}' returned record without required 'id' field")
557            if "data" not in record:
558                raise ValidationError(
559                    f"Polling trigger '{name}' returned record without required 'data' field")
560
561            # Validate record data against output schema
562            try:
563                validate(record["data"], trigger_config.output_schema)
564            except Exception as e:
565                raise ValidationError(e.message, e.schema, e.instance)
566            
567        return records

Execute a registered polling trigger

Arguments:
  • name: Name of the polling trigger to execute
  • inputs: Trigger inputs
  • last_poll_ts: Last poll timestamp
  • context: Execution context
Returns:

List of records

Raises:
  • ValidationError: If inputs or outputs don't match schema