diff --git a/docs/apidocs/autohive_integrations_sdk/integration.html b/docs/apidocs/autohive_integrations_sdk/integration.html index bec263d..275975a 100644 --- a/docs/apidocs/autohive_integrations_sdk/integration.html +++ b/docs/apidocs/autohive_integrations_sdk/integration.html @@ -30,6 +30,9 @@
Default User-Agent sent by ExecutionContext.fetch() when not overridden.
57class AuthType(Enum): -58 """Authentication strategy used by an integration. -59 -60 The platform stores the auth type alongside credentials and passes both -61 to ``ExecutionContext``. ``context.fetch()`` uses the type to decide -62 whether to auto-inject an ``Authorization`` header. -63 -64 Members: -65 PlatformOauth2: Platform-managed OAuth 2.0 — the platform handles the -66 token lifecycle and injects ``Bearer <access_token>`` automatically. -67 PlatformTeams: Platform-managed Microsoft Teams auth. -68 ApiKey: A single API key provided by the user. -69 Basic: Username/password (HTTP Basic) credentials. -70 Custom: Free-form credential fields defined by the integration's -71 ``config.json`` auth schema. The integration is responsible for -72 reading individual fields from ``context.auth``. -73 """ -74 PlatformOauth2 = "PlatformOauth2" -75 PlatformTeams = "PlatformTeams" -76 ApiKey = "ApiKey" -77 Basic = "Basic" -78 Custom = "Custom" +@@ -1443,11 +1520,11 @@70class AuthType(Enum): +71 """Authentication strategy used by an integration. +72 +73 The platform stores the auth type alongside credentials and passes both +74 to ``ExecutionContext``. ``context.fetch()`` uses the type to decide +75 whether to auto-inject an ``Authorization`` header. +76 +77 Members: +78 PlatformOauth2: Platform-managed OAuth 2.0 — the platform handles the +79 token lifecycle and injects ``Bearer <access_token>`` automatically. +80 PlatformTeams: Platform-managed Microsoft Teams auth. +81 ApiKey: A single API key provided by the user. +82 Basic: Username/password (HTTP Basic) credentials. +83 Custom: Free-form credential fields defined by the integration's +84 ``config.json`` auth schema. The integration is responsible for +85 reading individual fields from ``context.auth``. +86 """ +87 PlatformOauth2 = "PlatformOauth2" +88 PlatformTeams = "PlatformTeams" +89 ApiKey = "ApiKey" +90 Basic = "Basic" +91 Custom = "Custom"PlatformOauth2 = <AuthType.PlatformOauth2: 'PlatformOauth2'> - +
80class ResultType(Enum): -81 """Type of result being returned""" -82 ACTION = "action" -83 ACTION_ERROR = "action_error" -84 CONNECTED_ACCOUNT = "connected_account" -85 ERROR = "error" -86 VALIDATION_ERROR = "validation_error" +@@ -1529,11 +1606,11 @@93class ResultType(Enum): +94 """Type of result being returned""" +95 ACTION = "action" +96 ACTION_ERROR = "action_error" +97 CONNECTED_ACCOUNT = "connected_account" +98 ERROR = "error" +99 VALIDATION_ERROR = "validation_error"ACTION = <ResultType.ACTION: 'action'> - +
89class ValidationError(Exception): - 90 """Raised when SDK validation fails. - 91 - 92 This covers several cases: - 93 - 94 - Action inputs don't match the ``input_schema`` in ``config.json`` - 95 - Action outputs don't match the ``output_schema`` - 96 - Auth credentials don't match the ``auth.fields`` schema - 97 - An action handler returns something other than ``ActionResult`` - 98 - A handler name isn't registered - 99 """ -100 def __init__(self, message: str, schema: str = None, inputs: str = None, source: str = "legacy"): -101 self.schema = schema -102 """The schema that failed validation""" -103 self.inputs = inputs -104 """The data that failed validation""" -105 self.message = message -106 """The error message""" -107 self.source = source -108 """Where the validation failed: 'input', 'output', or 'legacy' (pre-versioning default)""" -109 super().__init__(message) +@@ -1637,37 +1714,37 @@102class ValidationError(Exception): +103 """Raised when SDK validation fails. +104 +105 This covers several cases: +106 +107 - Action inputs don't match the ``input_schema`` in ``config.json`` +108 - Action outputs don't match the ``output_schema`` +109 - Auth credentials don't match the ``auth.fields`` schema +110 - An action handler returns something other than ``ActionResult`` +111 - A handler name isn't registered +112 """ +113 def __init__(self, message: str, schema: str = None, inputs: str = None, source: str = "legacy"): +114 self.schema = schema +115 """The schema that failed validation""" +116 self.inputs = inputs +117 """The data that failed validation""" +118 self.message = message +119 """The error message""" +120 self.source = source +121 """Where the validation failed: 'input', 'output', or 'legacy' (pre-versioning default)""" +122 super().__init__(message)
@@ -2336,29 +2413,29 @@- + ValidationError( message: str, schema: str = None, inputs: str = None, source: str = 'legacy')-100 def __init__(self, message: str, schema: str = None, inputs: str = None, source: str = "legacy"): -101 self.schema = schema -102 """The schema that failed validation""" -103 self.inputs = inputs -104 """The data that failed validation""" -105 self.message = message -106 """The error message""" -107 self.source = source -108 """Where the validation failed: 'input', 'output', or 'legacy' (pre-versioning default)""" -109 super().__init__(message) +- +113 def __init__(self, message: str, schema: str = None, inputs: str = None, source: str = "legacy"): +114 self.schema = schema +115 """The schema that failed validation""" +116 self.inputs = inputs +117 """The data that failed validation""" +118 self.message = message +119 """The error message""" +120 self.source = source +121 """Where the validation failed: 'input', 'output', or 'legacy' (pre-versioning default)""" +122 super().__init__(message)schema - +- +@@ -1677,10 +1754,10 @@The schema that failed validation
inputs - +- +@@ -1690,10 +1767,10 @@The data that failed validation
message - +- +@@ -1703,10 +1780,10 @@The error message
source - +- +@@ -1716,7 +1793,7 @@Where the validation failed: 'input', 'output', or 'legacy' (pre-versioning default)
- + class ConfigurationError-(builtins.Exception): @@ -1724,9 +1801,9 @@
111class ConfigurationError(Exception): -112 """Raised when integration configuration is invalid""" -113 pass +@@ -1738,7 +1815,7 @@124class ConfigurationError(Exception): +125 """Raised when integration configuration is invalid""" +126 pass
- + class HTTPError-(builtins.Exception): @@ -1746,16 +1823,16 @@
115class HTTPError(Exception): -116 """Raised by ``ExecutionContext.fetch()`` for non-2xx HTTP responses (except 429).""" -117 def __init__(self, status: int, message: str, response_data: Any = None): -118 self.status = status -119 """Status code""" -120 self.message = message -121 """Error message""" -122 self.response_data = response_data -123 """Response data""" -124 super().__init__(f"HTTP {status}: {message}") +@@ -1766,35 +1843,35 @@128class HTTPError(Exception): +129 """Raised by ``ExecutionContext.fetch()`` for non-2xx HTTP responses (except 429).""" +130 def __init__(self, status: int, message: str, response_data: Any = None): +131 self.status = status +132 """Status code""" +133 self.message = message +134 """Error message""" +135 self.response_data = response_data +136 """Response data""" +137 super().__init__(f"HTTP {status}: {message}")
- + HTTPError(status: int, message: str, response_data: Any = None)-117 def __init__(self, status: int, message: str, response_data: Any = None): -118 self.status = status -119 """Status code""" -120 self.message = message -121 """Error message""" -122 self.response_data = response_data -123 """Response data""" -124 super().__init__(f"HTTP {status}: {message}") + - +status - +- +@@ -1804,10 +1881,10 @@Status code
message - +- +@@ -1817,10 +1894,10 @@Error message
response_data - +- +@@ -1830,7 +1907,7 @@Response data
- -126class RateLimitError(HTTPError): -127 """Raised by ``ExecutionContext.fetch()`` on HTTP 429 (Too Many Requests). -128 -129 Attributes: -130 retry_after: Seconds to wait before retrying, taken from the -131 ``Retry-After`` response header (defaults to 60 if absent). -132 """ -133 def __init__(self, retry_after: int, *args, **kwargs): -134 self.retry_after = retry_after -135 """Seconds to wait before retrying.""" -136 super().__init__(*args, **kwargs) +@@ -1863,31 +1940,31 @@139class RateLimitError(HTTPError): +140 """Raised by ``ExecutionContext.fetch()`` on HTTP 429 (Too Many Requests). +141 +142 Attributes: +143 retry_after: Seconds to wait before retrying, taken from the +144 ``Retry-After`` response header (defaults to 60 if absent). +145 """ +146 def __init__(self, retry_after: int, *args, **kwargs): +147 self.retry_after = retry_after +148 """Seconds to wait before retrying.""" +149 super().__init__(*args, **kwargs)
-- + RateLimitError(retry_after: int, *args, **kwargs)--133 def __init__(self, retry_after: int, *args, **kwargs): -134 self.retry_after = retry_after -135 """Seconds to wait before retrying.""" -136 super().__init__(*args, **kwargs) + - +-139@dataclass -140class FetchResponse: -141 """Response object returned by ``ExecutionContext.fetch()``. -142 -143 Wraps the full HTTP response so callers can inspect status codes and -144 headers in addition to the parsed body. -145 -146 Attributes: -147 status: HTTP status code (e.g. ``200``, ``201``). -148 headers: Response headers as a plain ``dict``. -149 data: Parsed JSON (``dict``/``list``) when the response is -150 ``application/json``, otherwise the raw response text. -151 ``None`` for empty 200/201/204 responses. -152 """ -153 status: int -154 headers: Dict[str, str] -155 data: Any +@@ -1953,47 +2030,47 @@152@dataclass +153class FetchResponse: +154 """Response object returned by ``ExecutionContext.fetch()``. +155 +156 Wraps the full HTTP response so callers can inspect status codes and +157 headers in addition to the parsed body. +158 +159 Attributes: +160 status: HTTP status code (e.g. ``200``, ``201``). +161 headers: Response headers as a plain ``dict``. +162 data: Parsed JSON (``dict``/``list``) when the response is +163 ``application/json``, otherwise the raw response text. +164 ``None`` for empty 200/201/204 responses. +165 """ +166 status: int +167 headers: Dict[str, str] +168 data: AnyInherited Members
@@ -2009,27 +2086,27 @@Inherited Members
@@ -2103,27 +2180,27 @@157@dataclass -158class ActionResult: -159 """Result returned by action handlers. -160 -161 This class encapsulates the data returned by an action along with optional -162 billing information for cost tracking. -163 -164 Args: -165 data: The actual result data from the action -166 cost_usd: Optional USD cost for billing purposes -167 -168 Example: -169 ```python -170 return ActionResult( -171 data={"message": "Success", "result": 42}, -172 cost_usd=0.05 -173 ) -174 ``` -175 """ -176 data: Any -177 cost_usd: Optional[float] = None +@@ -2057,25 +2134,25 @@170@dataclass +171class ActionResult: +172 """Result returned by action handlers. +173 +174 This class encapsulates the data returned by an action along with optional +175 billing information for cost tracking. +176 +177 Args: +178 data: The actual result data from the action +179 cost_usd: Optional USD cost for billing purposes +180 +181 Example: +182 ```python +183 return ActionResult( +184 data={"message": "Success", "result": 42}, +185 cost_usd=0.05 +186 ) +187 ``` +188 """ +189 data: Any +190 cost_usd: Optional[float] = NoneInherited Members
@@ -2083,11 +2160,11 @@- - + +Inherited Members
cost_usd: Optional[float] = None - +Inherited Members
@@ -2197,23 +2274,23 @@179@dataclass -180class ActionError: -181 """Error result returned by action handlers for expected/application-level errors. -182 -183 When returned from an action handler, output schema validation is skipped -184 and the error is returned to the caller as a ResultType.ERROR result. -185 -186 Args: -187 message: Human-readable error message -188 cost_usd: Optional USD cost incurred before the error occurred -189 -190 Example: -191 ```python -192 return ActionError( -193 message="User not found", -194 cost_usd=0.01 -195 ) -196 ``` -197 """ -198 message: str -199 cost_usd: Optional[float] = None +@@ -2151,25 +2228,25 @@192@dataclass +193class ActionError: +194 """Error result returned by action handlers for expected/application-level errors. +195 +196 When returned from an action handler, output schema validation is skipped +197 and the error is returned to the caller as a ResultType.ERROR result. +198 +199 Args: +200 message: Human-readable error message +201 cost_usd: Optional USD cost incurred before the error occurred +202 +203 Example: +204 ```python +205 return ActionError( +206 message="User not found", +207 cost_usd=0.01 +208 ) +209 ``` +210 """ +211 message: str +212 cost_usd: Optional[float] = NoneInherited Members
@@ -2177,11 +2254,11 @@- - + +Inherited Members
cost_usd: Optional[float] = None - +Inherited Members
201@dataclass -202class ConnectedAccountInfo: -203 """Account metadata returned by a ``ConnectedAccountHandler``. -204 -205 The platform calls the connected-account handler after a user links -206 their external account. The returned info is displayed in the -207 Autohive UI (avatar, name, email, etc.). -208 -209 All fields are optional — populate whichever ones the external API provides. -210 """ -211 email: Optional[str] = None -212 first_name: Optional[str] = None -213 last_name: Optional[str] = None -214 username: Optional[str] = None -215 user_id: Optional[str] = None -216 avatar_url: Optional[str] = None -217 organization: Optional[str] = None +@@ -2229,14 +2306,14 @@214@dataclass +215class ConnectedAccountInfo: +216 """Account metadata returned by a ``ConnectedAccountHandler``. +217 +218 The platform calls the connected-account handler after a user links +219 their external account. The returned info is displayed in the +220 Autohive UI (avatar, name, email, etc.). +221 +222 All fields are optional — populate whichever ones the external API provides. +223 """ +224 email: Optional[str] = None +225 first_name: Optional[str] = None +226 last_name: Optional[str] = None +227 username: Optional[str] = None +228 user_id: Optional[str] = None +229 avatar_url: Optional[str] = None +230 organization: Optional[str] = NoneInherited Members
- + ConnectedAccountInfo( email: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, username: Optional[str] = None, user_id: Optional[str] = None, avatar_url: Optional[str] = None, organization: Optional[str] = None) - +- - + +@@ -2244,11 +2321,11 @@- - + +Inherited Members
email: Optional[str] = None - +@@ -2256,11 +2333,11 @@- - + +Inherited Members
first_name: Optional[str] = None - +@@ -2268,11 +2345,11 @@- - + +Inherited Members
last_name: Optional[str] = None - +@@ -2280,11 +2357,11 @@- - + +Inherited Members
username: Optional[str] = None - +@@ -2292,11 +2369,11 @@- - + +Inherited Members
user_id: Optional[str] = None - +@@ -2304,11 +2381,11 @@- - + +Inherited Members
avatar_url: Optional[str] = None - +@@ -2316,11 +2393,11 @@- - + +Inherited Members
organization: Optional[str] = None - +Inherited Members
219@dataclass -220class IntegrationResult: -221 """Result format sent from lambda wrapper to backend. -222 -223 This class represents the standardized format that the lambda wrapper -224 sends to the Autohive backend, including SDK version and type-specific data. -225 -226 Args: -227 version: SDK version (auto-populated) -228 type: Type of result payload (ResultType enum: ACTION, CONNECTED_ACCOUNT, ERROR) -229 result: The result object - ActionResult for actions, ActionError for -230 application-level action errors, or ConnectedAccountInfo for -231 connected accounts. -232 The lambda wrapper serializes these to dicts using asdict(). -233 -234 Note: -235 This type is returned by Integration methods and serialized by the lambda wrapper. -236 Integration developers should use ActionResult for action handlers and -237 ActionError for expected error conditions. -238 """ -239 version: str -240 type: ResultType -241 result: Union[ActionResult, ActionError, ConnectedAccountInfo] +@@ -2384,47 +2461,47 @@232@dataclass +233class IntegrationResult: +234 """Result format sent from lambda wrapper to backend. +235 +236 This class represents the standardized format that the lambda wrapper +237 sends to the Autohive backend, including SDK version and type-specific data. +238 +239 Args: +240 version: SDK version (auto-populated) +241 type: Type of result payload (ResultType enum: ACTION, CONNECTED_ACCOUNT, ERROR) +242 result: The result object - ActionResult for actions, ActionError for +243 application-level action errors, or ConnectedAccountInfo for +244 connected accounts. +245 The lambda wrapper serializes these to dicts using asdict(). +246 +247 Note: +248 This type is returned by Integration methods and serialized by the lambda wrapper. +249 Integration developers should use ActionResult for action handlers and +250 ActionError for expected error conditions. +251 """ +252 version: str +253 type: ResultType +254 result: Union[ActionResult, ActionError, ConnectedAccountInfo]Inherited Members
- + IntegrationResult( version: str, type: ResultType, result: Union[ActionResult, ActionError, ConnectedAccountInfo]) - +- - + +type: ResultType - +- - + +@@ -2440,15 +2517,15 @@result: Union[ActionResult, ActionError, ConnectedAccountInfo] - +- - + +Inherited Members
245@dataclass -246class Parameter: -247 """Definition of a parameter""" -248 name: str -249 type: str -250 description: str -251 enum: Optional[List[str]] = None -252 required: bool = True -253 default: Any = None +@@ -2458,47 +2535,47 @@258@dataclass +259class Parameter: +260 """Definition of a parameter""" +261 name: str +262 type: str +263 description: str +264 enum: Optional[List[str]] = None +265 required: bool = True +266 default: Any = NoneInherited Members
- + Parameter( name: str, type: str, description: str, enum: Optional[List[str]] = None, required: bool = True, default: Any = None) - +- - + +@@ -2506,11 +2583,11 @@- - + +Inherited Members
enum: Optional[List[str]] = None - +
255@dataclass -256class SchemaDefinition: -257 """Base class for components that have input/output schemas""" -258 name: str -259 description: str -260 input_schema: List[Parameter] -261 output_schema: Optional[Dict[str, Any]] = None +@@ -2566,47 +2643,47 @@268@dataclass +269class SchemaDefinition: +270 """Base class for components that have input/output schemas""" +271 name: str +272 description: str +273 input_schema: List[Parameter] +274 output_schema: Optional[Dict[str, Any]] = NoneInherited Members
- + SchemaDefinition( name: str, description: str, input_schema: List[Parameter], output_schema: Optional[Dict[str, Any]] = None) - +- - + +input_schema: List[Parameter] - +- - + +@@ -2614,11 +2691,11 @@- - + +Inherited Members
output_schema: Optional[Dict[str, Any]] = None - +
263@dataclass -264class Action(SchemaDefinition): -265 """Empty dataclass that inherits from SchemaDefinition""" -266 pass +@@ -2647,14 +2724,14 @@276@dataclass +277class Action(SchemaDefinition): +278 """Empty dataclass that inherits from SchemaDefinition""" +279 passInherited Members
- + Action( name: str, description: str, input_schema: List[Parameter], output_schema: Optional[Dict[str, Any]] = None) - +- - + +@@ -2682,10 +2759,10 @@-Inherited Members
268@dataclass -269class PollingTrigger(SchemaDefinition): -270 """Definition of a polling trigger""" -271 polling_interval: timedelta = field(default_factory=timedelta) +@@ -2695,25 +2772,25 @@281@dataclass +282class PollingTrigger(SchemaDefinition): +283 """Definition of a polling trigger""" +284 polling_interval: timedelta = field(default_factory=timedelta)Inherited Members
- + PollingTrigger( name: str, description: str, input_schema: List[Parameter], output_schema: Optional[Dict[str, Any]] = None, polling_interval: datetime.timedelta = <factory>) - +- - + +@@ -2741,15 +2818,15 @@-Inherited Members
273@dataclass -274class IntegrationConfig: -275 """Configuration for an integration""" -276 name: str -277 version: str -278 description: str -279 auth: Dict[str, Any] -280 actions: Dict[str, Action] -281 polling_triggers: Dict[str, PollingTrigger] +@@ -2759,87 +2836,87 @@286@dataclass +287class IntegrationConfig: +288 """Configuration for an integration""" +289 name: str +290 version: str +291 description: str +292 auth: Dict[str, Any] +293 actions: Dict[str, Action] +294 polling_triggers: Dict[str, PollingTrigger]Inherited Members
- + IntegrationConfig( name: str, version: str, description: str, auth: Dict[str, Any], actions: Dict[str, Action], polling_triggers: Dict[str, PollingTrigger]) - +- - + +actions: Dict[str, Action] - +- - + +polling_triggers: Dict[str, PollingTrigger] - +- - + + - + class ActionHandler-(abc.ABC): @@ -2847,32 +2924,32 @@ Inherited Members
-284class ActionHandler(ABC): -285 """Base class for action handlers. -286 -287 Subclass this and implement ``execute()`` to handle a specific action. -288 Register it with the ``@integration.action("action_name")`` decorator. -289 -290 Example:: -291 -292 @integration.action("get_user") -293 class GetUser(ActionHandler): -294 async def execute(self, inputs, context): -295 user = (await context.fetch(f"https://api.example.com/users/{inputs['id']}")).data -296 return ActionResult(data=user) -297 """ -298 @abstractmethod -299 async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any: -300 """Run the action logic. -301 -302 Args: -303 inputs: Validated action inputs matching the ``input_schema`` from ``config.json``. -304 context: Execution context providing ``fetch()``, ``auth``, and logging. -305 -306 Returns: -307 An ``ActionResult`` containing the output data and optional ``cost_usd``. -308 """ -309 pass +@@ -2904,18 +2981,18 @@297class ActionHandler(ABC): +298 """Base class for action handlers. +299 +300 Subclass this and implement ``execute()`` to handle a specific action. +301 Register it with the ``@integration.action("action_name")`` decorator. +302 +303 Example:: +304 +305 @integration.action("get_user") +306 class GetUser(ActionHandler): +307 async def execute(self, inputs, context): +308 user = (await context.fetch(f"https://api.example.com/users/{inputs['id']}")).data +309 return ActionResult(data=user) +310 """ +311 @abstractmethod +312 async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any: +313 """Run the action logic. +314 +315 Args: +316 inputs: Validated action inputs matching the ``input_schema`` from ``config.json``. +317 context: Execution context providing ``fetch()``, ``auth``, and logging. +318 +319 Returns: +320 An ``ActionResult`` containing the output data and optional ``cost_usd``. +321 """ +322 passInherited Members
298 @abstractmethod -299 async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any: -300 """Run the action logic. -301 -302 Args: -303 inputs: Validated action inputs matching the ``input_schema`` from ``config.json``. -304 context: Execution context providing ``fetch()``, ``auth``, and logging. -305 -306 Returns: -307 An ``ActionResult`` containing the output data and optional ``cost_usd``. -308 """ -309 pass +@@ -2935,7 +3012,7 @@311 @abstractmethod +312 async def execute(self, inputs: Dict[str, Any], context: 'ExecutionContext') -> Any: +313 """Run the action logic. +314 +315 Args: +316 inputs: Validated action inputs matching the ``input_schema`` from ``config.json``. +317 context: Execution context providing ``fetch()``, ``auth``, and logging. +318 +319 Returns: +320 An ``ActionResult`` containing the output data and optional ``cost_usd``. +321 """ +322 passInherited Members
- + class PollingTriggerHandler-(abc.ABC): @@ -2943,12 +3020,12 @@ Inherited Members
-311class PollingTriggerHandler(ABC): -312 """Base class for polling trigger handlers""" -313 @abstractmethod -314 async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]: -315 """Execute the polling trigger""" -316 pass +@@ -2968,10 +3045,10 @@324class PollingTriggerHandler(ABC): +325 """Base class for polling trigger handlers""" +326 @abstractmethod +327 async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]: +328 """Execute the polling trigger""" +329 passInherited Members
313 @abstractmethod -314 async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]: -315 """Execute the polling trigger""" -316 pass +@@ -2984,7 +3061,7 @@326 @abstractmethod +327 async def poll(self, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: 'ExecutionContext') -> List[Dict[str, Any]]: +328 """Execute the polling trigger""" +329 passInherited Members
- + class ConnectedAccountHandler-(abc.ABC): @@ -2992,37 +3069,37 @@ Inherited Members
-318class ConnectedAccountHandler(ABC): -319 """Base class for connected-account handlers. -320 -321 The platform calls this after a user links their external account. -322 The returned ``ConnectedAccountInfo`` is shown in the Autohive UI. -323 -324 Register with the ``@integration.connected_account()`` decorator. -325 -326 Example:: -327 -328 @integration.connected_account() -329 class MyAccountHandler(ConnectedAccountHandler): -330 async def get_account_info(self, context): -331 me = (await context.fetch("https://api.example.com/me")).data -332 return ConnectedAccountInfo( -333 email=me["email"], -334 first_name=me["first_name"], -335 last_name=me["last_name"], -336 ) -337 """ -338 @abstractmethod -339 async def get_account_info(self, context: 'ExecutionContext') -> ConnectedAccountInfo: -340 """Fetch account metadata from the external service. -341 -342 For platform OAuth integrations, ``context.fetch()`` auto-injects -343 the Bearer token — no manual auth handling needed. -344 -345 Returns: -346 A ``ConnectedAccountInfo`` with whichever fields the API provides. -347 """ -348 pass +@@ -3060,17 +3137,17 @@331class ConnectedAccountHandler(ABC): +332 """Base class for connected-account handlers. +333 +334 The platform calls this after a user links their external account. +335 The returned ``ConnectedAccountInfo`` is shown in the Autohive UI. +336 +337 Register with the ``@integration.connected_account()`` decorator. +338 +339 Example:: +340 +341 @integration.connected_account() +342 class MyAccountHandler(ConnectedAccountHandler): +343 async def get_account_info(self, context): +344 me = (await context.fetch("https://api.example.com/me")).data +345 return ConnectedAccountInfo( +346 email=me["email"], +347 first_name=me["first_name"], +348 last_name=me["last_name"], +349 ) +350 """ +351 @abstractmethod +352 async def get_account_info(self, context: 'ExecutionContext') -> ConnectedAccountInfo: +353 """Fetch account metadata from the external service. +354 +355 For platform OAuth integrations, ``context.fetch()`` auto-injects +356 the Bearer token — no manual auth handling needed. +357 +358 Returns: +359 A ``ConnectedAccountInfo`` with whichever fields the API provides. +360 """ +361 passInherited Members
338 @abstractmethod -339 async def get_account_info(self, context: 'ExecutionContext') -> ConnectedAccountInfo: -340 """Fetch account metadata from the external service. -341 -342 For platform OAuth integrations, ``context.fetch()`` auto-injects -343 the Bearer token — no manual auth handling needed. -344 -345 Returns: -346 A ``ConnectedAccountInfo`` with whichever fields the API provides. -347 """ -348 pass +@@ -3089,7 +3166,7 @@351 @abstractmethod +352 async def get_account_info(self, context: 'ExecutionContext') -> ConnectedAccountInfo: +353 """Fetch account metadata from the external service. +354 +355 For platform OAuth integrations, ``context.fetch()`` auto-injects +356 the Bearer token — no manual auth handling needed. +357 +358 Returns: +359 A ``ConnectedAccountInfo`` with whichever fields the API provides. +360 """ +361 passInherited Members
- + class ExecutionContext: @@ -3097,213 +3174,246 @@-Inherited Members
351class ExecutionContext: -352 """Context provided to integration handlers for making authenticated HTTP requests. -353 -354 Manages an ``aiohttp`` session with automatic retries, error handling, and -355 optional Bearer-token injection for platform OAuth integrations. -356 -357 Use as an async context manager:: -358 -359 async with ExecutionContext(auth=auth) as context: -360 result = await integration.execute_action("my_action", inputs, context) -361 -362 Args: -363 auth: Authentication data. In **local tests** this is a flat dict -364 matching the ``auth.fields`` schema in ``config.json`` -365 (e.g. ``{"api_key": "..."}``). In **production** the platform -366 wraps credentials as ``{"auth_type": "...", "credentials": {...}}``. -367 request_config: Override default ``max_retries`` (3) and ``timeout`` (30 s). -368 metadata: Arbitrary metadata forwarded to handlers. -369 logger: Custom logger; falls back to ``logging.getLogger(__name__)``. -370 """ -371 def __init__( -372 self, -373 auth: Dict[str, Any] = {}, -374 request_config: Optional[Dict[str, Any]] = None, -375 metadata: Optional[Dict[str, Any]] = None, -376 logger: Optional[logging.Logger] = None -377 ): -378 self.auth = auth -379 """Authentication configuration""" -380 self.config = request_config or {"max_retries": 3, "timeout": 30} -381 """Request configuration""" -382 self.metadata = metadata or {} -383 """Additional metadata""" -384 self.logger = logger or logging.getLogger(__name__) -385 """Logger instance""" -386 self._session: Optional[aiohttp.ClientSession] = None -387 -388 async def __aenter__(self): -389 if not self._session: -390 self._session = aiohttp.ClientSession() -391 return self -392 -393 async def __aexit__(self, exc_type, exc_val, exc_tb): -394 if self._session: -395 await self._session.close() -396 self._session = None -397 -398 async def fetch( -399 self, -400 url: str, -401 method: str = "GET", -402 params: Optional[Dict[str, Any]] = None, -403 data: Any = None, -404 json: Any = None, -405 headers: Optional[Dict[str, str]] = None, -406 content_type: Optional[str] = None, -407 timeout: Optional[int] = None, -408 retry_count: int = 0 -409 ) -> FetchResponse: -410 """Make an HTTP request with automatic retries and error handling. -411 -412 For **platform OAuth** integrations (``auth_type == "PlatformOauth2"``), -413 a ``Bearer`` token is auto-injected from ``auth.credentials.access_token`` -414 unless an ``Authorization`` header is explicitly provided. -415 -416 Retries up to ``max_retries`` (default 3) on transient network errors -417 with exponential back-off. HTTP 429 responses raise ``RateLimitError`` -418 immediately (no automatic retry). -419 -420 Args: -421 url: The URL to request. -422 method: HTTP method (``"GET"``, ``"POST"``, ``"PUT"``, etc.). -423 params: Query parameters appended to the URL. Nested dicts/lists -424 are JSON-serialized automatically. -425 data: Raw request body. Encoding depends on ``content_type``. -426 json: JSON-serializable payload. Sets ``content_type`` to -427 ``application/json`` automatically. -428 headers: Additional HTTP headers. Merged *after* any auto-injected -429 auth header, so an explicit ``Authorization`` takes precedence. -430 content_type: ``Content-Type`` header value. -431 timeout: Per-request timeout in seconds (overrides ``request_config``). -432 retry_count: Internal — current retry attempt number. -433 -434 Returns: -435 A ``FetchResponse`` containing the HTTP status code, response -436 headers, and parsed body data. -437 -438 Raises: -439 RateLimitError: On HTTP 429 with the ``Retry-After`` value. -440 HTTPError: On any other non-2xx status. -441 """ -442 if not self._session: -443 self._session = aiohttp.ClientSession() -444 -445 # Prepare request -446 if json is not None: -447 data = json -448 content_type = "application/json" +364class ExecutionContext: +365 """Context provided to integration handlers for making authenticated HTTP requests. +366 +367 Manages an ``aiohttp`` session with automatic retries, error handling, +368 default ``User-Agent`` handling, and optional Bearer-token injection for +369 platform OAuth integrations. +370 +371 Use as an async context manager:: +372 +373 async with ExecutionContext(auth=auth) as context: +374 result = await integration.execute_action("my_action", inputs, context) +375 +376 Args: +377 auth: Authentication data. In **local tests** this is a flat dict +378 matching the ``auth.fields`` schema in ``config.json`` +379 (e.g. ``{"api_key": "..."}``). In **production** the platform +380 wraps credentials as ``{"auth_type": "...", "credentials": {...}}``. +381 request_config: Override default ``max_retries`` (3) and ``timeout`` (30 s). +382 metadata: Arbitrary metadata forwarded to handlers. +383 logger: Custom logger; falls back to ``logging.getLogger(__name__)``. +384 """ +385 def __init__( +386 self, +387 auth: Dict[str, Any] = {}, +388 request_config: Optional[Dict[str, Any]] = None, +389 metadata: Optional[Dict[str, Any]] = None, +390 logger: Optional[logging.Logger] = None +391 ): +392 self.auth = auth +393 """Authentication configuration""" +394 self.config = request_config or {"max_retries": 3, "timeout": 30} +395 """Request configuration""" +396 self.metadata = metadata or {} +397 """Additional metadata""" +398 self.logger = logger or logging.getLogger(__name__) +399 """Logger instance""" +400 self._session: Optional[aiohttp.ClientSession] = None +401 self._integration_name: Optional[str] = None +402 self._integration_version: Optional[str] = None +403 +404 async def __aenter__(self): +405 if not self._session: +406 self._session = aiohttp.ClientSession() +407 return self +408 +409 async def __aexit__(self, exc_type, exc_val, exc_tb): +410 if self._session: +411 await self._session.close() +412 self._session = None +413 +414 def _set_integration_identity(self, name: Optional[str], version: Optional[str]) -> None: +415 """Attach integration identity for SDK-generated request metadata.""" +416 self._integration_name = name +417 self._integration_version = version +418 +419 def _build_default_user_agent(self) -> str: +420 if self._integration_name and self._integration_version: +421 integration_token = ( +422 f"{_sanitize_user_agent_token(self._integration_name)}/" +423 f"{_sanitize_user_agent_token(self._integration_version)}" +424 ) +425 return f"{DEFAULT_USER_AGENT} {integration_token}" +426 +427 return DEFAULT_USER_AGENT +428 +429 async def fetch( +430 self, +431 url: str, +432 method: str = "GET", +433 params: Optional[Dict[str, Any]] = None, +434 data: Any = None, +435 json: Any = None, +436 headers: Optional[Dict[str, str]] = None, +437 content_type: Optional[str] = None, +438 timeout: Optional[int] = None, +439 retry_count: int = 0, +440 user_agent: Optional[str] = None +441 ) -> FetchResponse: +442 """Make an HTTP request with automatic retries and error handling. +443 +444 If no ``User-Agent`` header is provided, a default SDK ``User-Agent`` is +445 added. When the request is made inside a handler executed by +446 ``Integration``, the integration's ``config.json`` name and version are +447 included. Pass ``user_agent`` to set a per-request value more easily. +448 Explicit ``User-Agent`` headers always take precedence. 449 -450 final_headers = {} -451 -452 if self.auth and "Authorization" not in (headers or {}): -453 auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2")) -454 credentials = self.auth.get("credentials", {}) -455 -456 if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials: -457 final_headers["Authorization"] = f"Bearer {credentials['access_token']}" -458 -459 if content_type: -460 final_headers["Content-Type"] = content_type -461 if headers: -462 final_headers.update(headers) -463 -464 if params: -465 # Handle nested dictionary parameters -466 flat_params = {} -467 for key, value in params.items(): -468 if isinstance(value, (dict, list)): -469 flat_params[key] = jsonX.dumps(value) -470 elif value is not None: -471 flat_params[key] = str(value) -472 query_string = urlencode(flat_params) -473 url = f"{url}{'&' if '?' in url else '?'}{query_string}" +450 For **platform OAuth** integrations (``auth_type == "PlatformOauth2"``), +451 a ``Bearer`` token is auto-injected from ``auth.credentials.access_token`` +452 unless an ``Authorization`` header is explicitly provided. +453 +454 Retries up to ``max_retries`` (default 3) on transient network errors +455 with exponential back-off. HTTP 429 responses raise ``RateLimitError`` +456 immediately (no automatic retry). +457 +458 Args: +459 url: The URL to request. +460 method: HTTP method (``"GET"``, ``"POST"``, ``"PUT"``, etc.). +461 params: Query parameters appended to the URL. Nested dicts/lists +462 are JSON-serialized automatically. +463 data: Raw request body. Encoding depends on ``content_type``. +464 json: JSON-serializable payload. Sets ``content_type`` to +465 ``application/json`` automatically. +466 headers: Additional HTTP headers. Merged *after* any auto-injected +467 auth header, so explicit ``Authorization`` and ``User-Agent`` +468 values take precedence. +469 user_agent: Convenience override for the request ``User-Agent``. +470 Ignored when ``headers`` already contains a ``User-Agent`` key. +471 content_type: ``Content-Type`` header value. +472 timeout: Per-request timeout in seconds (overrides ``request_config``). +473 retry_count: Internal — current retry attempt number. 474 -475 # Prepare body -476 if data is not None: -477 if content_type == "application/json": -478 data = jsonX.dumps(data) -479 elif content_type == "application/x-www-form-urlencoded": -480 data = urlencode(data) if isinstance(data, dict) else data -481 -482 # Store the original timeout numeric value -483 original_timeout = timeout or self.config["timeout"] -484 -485 # Convert the numeric timeout to a ClientTimeout instance for this request -486 client_timeout = aiohttp.ClientTimeout(total=original_timeout) -487 -488 try: -489 async with self._session.request( -490 method=method, -491 url=url, -492 data=data, -493 headers=final_headers, -494 timeout=client_timeout, -495 ssl=True -496 ) as response: -497 content_type = response.headers.get("Content-Type", "") -498 -499 if response.status == 429: # Rate limit -500 retry_after = int(response.headers.get("Retry-After", 60)) -501 raise RateLimitError( -502 retry_after, -503 response.status, -504 "Rate limit exceeded", -505 await response.text() -506 ) +475 Returns: +476 A ``FetchResponse`` containing the HTTP status code, response +477 headers, and parsed body data. +478 +479 Raises: +480 RateLimitError: On HTTP 429 with the ``Retry-After`` value. +481 HTTPError: On any other non-2xx status. +482 """ +483 if not self._session: +484 self._session = aiohttp.ClientSession() +485 +486 # Prepare request +487 if json is not None: +488 data = json +489 content_type = "application/json" +490 +491 final_headers = {} +492 +493 if not any(key.lower() == "user-agent" for key in (headers or {})): +494 final_headers["User-Agent"] = user_agent or self._build_default_user_agent() +495 +496 if self.auth and "Authorization" not in (headers or {}): +497 auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2")) +498 credentials = self.auth.get("credentials", {}) +499 +500 if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials: +501 final_headers["Authorization"] = f"Bearer {credentials['access_token']}" +502 +503 if content_type: +504 final_headers["Content-Type"] = content_type +505 if headers: +506 final_headers.update(headers) 507 -508 try: -509 if "application/json" in content_type: -510 result = await response.json() -511 else: -512 result = await response.text() -513 if not result and response.status in {200, 201, 204}: -514 result = None -515 except Exception as e: -516 self.logger.error(f"Error parsing response: {e}") -517 result = await response.text() +508 if params: +509 # Handle nested dictionary parameters +510 flat_params = {} +511 for key, value in params.items(): +512 if isinstance(value, (dict, list)): +513 flat_params[key] = jsonX.dumps(value) +514 elif value is not None: +515 flat_params[key] = str(value) +516 query_string = urlencode(flat_params) +517 url = f"{url}{'&' if '?' in url else '?'}{query_string}" 518 -519 response_headers = dict(response.headers) -520 -521 if not response.ok: -522 print(f"HTTP error encountered. Status: {response.status}. Result: {result}") -523 raise HTTPError(response.status, str(result), result) -524 -525 return FetchResponse( -526 status=response.status, -527 headers=response_headers, -528 data=result, -529 ) -530 -531 except RateLimitError: -532 raise -533 except (aiohttp.ClientError, asyncio.TimeoutError) as e: -534 # Don't want to send this to Raygun here because this will be retried. -535 print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.") -536 if retry_count < self.config["max_retries"]: -537 await asyncio.sleep(2 ** retry_count) # Exponential backoff -538 print("Retrying request...") -539 # Use original_timeout (numeric) for recursive calls -540 return await self.fetch( -541 url, method, params, data, json, -542 headers, content_type, original_timeout, retry_count + 1 -543 ) -544 else: -545 print("Max retries reached. Raising error.") -546 raise -547 except Exception as e: -548 self.logger.error(f"Unexpected error during {method} {url}: {e}") -549 print(f"Unexpected error encountered: {e}") -550 raise +519 # Prepare body +520 if data is not None: +521 if content_type == "application/json": +522 data = jsonX.dumps(data) +523 elif content_type == "application/x-www-form-urlencoded": +524 data = urlencode(data) if isinstance(data, dict) else data +525 +526 # Store the original timeout numeric value +527 original_timeout = timeout or self.config["timeout"] +528 +529 # Convert the numeric timeout to a ClientTimeout instance for this request +530 client_timeout = aiohttp.ClientTimeout(total=original_timeout) +531 +532 try: +533 async with self._session.request( +534 method=method, +535 url=url, +536 data=data, +537 headers=final_headers, +538 timeout=client_timeout, +539 ssl=True +540 ) as response: +541 content_type = response.headers.get("Content-Type", "") +542 +543 if response.status == 429: # Rate limit +544 retry_after = int(response.headers.get("Retry-After", 60)) +545 raise RateLimitError( +546 retry_after, +547 response.status, +548 "Rate limit exceeded", +549 await response.text() +550 ) +551 +552 try: +553 if "application/json" in content_type: +554 result = await response.json() +555 else: +556 result = await response.text() +557 if not result and response.status in {200, 201, 204}: +558 result = None +559 except Exception as e: +560 self.logger.error(f"Error parsing response: {e}") +561 result = await response.text() +562 +563 response_headers = dict(response.headers) +564 +565 if not response.ok: +566 print(f"HTTP error encountered. Status: {response.status}. Result: {result}") +567 raise HTTPError(response.status, str(result), result) +568 +569 return FetchResponse( +570 status=response.status, +571 headers=response_headers, +572 data=result, +573 ) +574 +575 except RateLimitError: +576 raise +577 except (aiohttp.ClientError, asyncio.TimeoutError) as e: +578 # Don't want to send this to Raygun here because this will be retried. +579 print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.") +580 if retry_count < self.config["max_retries"]: +581 await asyncio.sleep(2 ** retry_count) # Exponential backoff +582 print("Retrying request...") +583 # Use original_timeout (numeric) for recursive calls +584 return await self.fetch( +585 url, method, params, data, json, +586 headers, content_type, original_timeout, retry_count + 1, +587 user_agent=user_agent, +588 ) +589 else: +590 print("Max retries reached. Raising error.") +591 raise +592 except Exception as e: +593 self.logger.error(f"Unexpected error during {method} {url}: {e}") +594 print(f"Unexpected error encountered: {e}") +595 raiseContext provided to integration handlers for making authenticated HTTP requests.
-Manages an
+aiohttpsession with automatic retries, error handling, and -optional Bearer-token injection for platform OAuth integrations.Manages an
aiohttpsession with automatic retries, error handling, +defaultUser-Agenthandling, and optional Bearer-token injection for +platform OAuth integrations.Use as an async context manager::
@@ -3325,43 +3435,45 @@Inherited Members
- + ExecutionContext( auth: Dict[str, Any] = {}, request_config: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None, logger: Optional[logging.Logger] = None)-371 def __init__( -372 self, -373 auth: Dict[str, Any] = {}, -374 request_config: Optional[Dict[str, Any]] = None, -375 metadata: Optional[Dict[str, Any]] = None, -376 logger: Optional[logging.Logger] = None -377 ): -378 self.auth = auth -379 """Authentication configuration""" -380 self.config = request_config or {"max_retries": 3, "timeout": 30} -381 """Request configuration""" -382 self.metadata = metadata or {} -383 """Additional metadata""" -384 self.logger = logger or logging.getLogger(__name__) -385 """Logger instance""" -386 self._session: Optional[aiohttp.ClientSession] = None +- +385 def __init__( +386 self, +387 auth: Dict[str, Any] = {}, +388 request_config: Optional[Dict[str, Any]] = None, +389 metadata: Optional[Dict[str, Any]] = None, +390 logger: Optional[logging.Logger] = None +391 ): +392 self.auth = auth +393 """Authentication configuration""" +394 self.config = request_config or {"max_retries": 3, "timeout": 30} +395 """Request configuration""" +396 self.metadata = metadata or {} +397 """Additional metadata""" +398 self.logger = logger or logging.getLogger(__name__) +399 """Logger instance""" +400 self._session: Optional[aiohttp.ClientSession] = None +401 self._integration_name: Optional[str] = None +402 self._integration_version: Optional[str] = Noneauth - +- +@@ -3371,10 +3483,10 @@Authentication configuration
Inherited Members
config - +- +@@ -3384,10 +3496,10 @@Request configuration
Inherited Members
metadata - +- +@@ -3397,10 +3509,10 @@Additional metadata
Inherited Members
logger - +- +@@ -3409,172 +3521,192 @@Logger instance
Inherited Members
- + 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) -> FetchResponse: + 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, user_agent: Optional[str] = None) -> FetchResponse:-398 async def fetch( -399 self, -400 url: str, -401 method: str = "GET", -402 params: Optional[Dict[str, Any]] = None, -403 data: Any = None, -404 json: Any = None, -405 headers: Optional[Dict[str, str]] = None, -406 content_type: Optional[str] = None, -407 timeout: Optional[int] = None, -408 retry_count: int = 0 -409 ) -> FetchResponse: -410 """Make an HTTP request with automatic retries and error handling. -411 -412 For **platform OAuth** integrations (``auth_type == "PlatformOauth2"``), -413 a ``Bearer`` token is auto-injected from ``auth.credentials.access_token`` -414 unless an ``Authorization`` header is explicitly provided. -415 -416 Retries up to ``max_retries`` (default 3) on transient network errors -417 with exponential back-off. HTTP 429 responses raise ``RateLimitError`` -418 immediately (no automatic retry). -419 -420 Args: -421 url: The URL to request. -422 method: HTTP method (``"GET"``, ``"POST"``, ``"PUT"``, etc.). -423 params: Query parameters appended to the URL. Nested dicts/lists -424 are JSON-serialized automatically. -425 data: Raw request body. Encoding depends on ``content_type``. -426 json: JSON-serializable payload. Sets ``content_type`` to -427 ``application/json`` automatically. -428 headers: Additional HTTP headers. Merged *after* any auto-injected -429 auth header, so an explicit ``Authorization`` takes precedence. -430 content_type: ``Content-Type`` header value. -431 timeout: Per-request timeout in seconds (overrides ``request_config``). -432 retry_count: Internal — current retry attempt number. -433 -434 Returns: -435 A ``FetchResponse`` containing the HTTP status code, response -436 headers, and parsed body data. -437 -438 Raises: -439 RateLimitError: On HTTP 429 with the ``Retry-After`` value. -440 HTTPError: On any other non-2xx status. -441 """ -442 if not self._session: -443 self._session = aiohttp.ClientSession() -444 -445 # Prepare request -446 if json is not None: -447 data = json -448 content_type = "application/json" +429 async def fetch( +430 self, +431 url: str, +432 method: str = "GET", +433 params: Optional[Dict[str, Any]] = None, +434 data: Any = None, +435 json: Any = None, +436 headers: Optional[Dict[str, str]] = None, +437 content_type: Optional[str] = None, +438 timeout: Optional[int] = None, +439 retry_count: int = 0, +440 user_agent: Optional[str] = None +441 ) -> FetchResponse: +442 """Make an HTTP request with automatic retries and error handling. +443 +444 If no ``User-Agent`` header is provided, a default SDK ``User-Agent`` is +445 added. When the request is made inside a handler executed by +446 ``Integration``, the integration's ``config.json`` name and version are +447 included. Pass ``user_agent`` to set a per-request value more easily. +448 Explicit ``User-Agent`` headers always take precedence. 449 -450 final_headers = {} -451 -452 if self.auth and "Authorization" not in (headers or {}): -453 auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2")) -454 credentials = self.auth.get("credentials", {}) -455 -456 if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials: -457 final_headers["Authorization"] = f"Bearer {credentials['access_token']}" -458 -459 if content_type: -460 final_headers["Content-Type"] = content_type -461 if headers: -462 final_headers.update(headers) -463 -464 if params: -465 # Handle nested dictionary parameters -466 flat_params = {} -467 for key, value in params.items(): -468 if isinstance(value, (dict, list)): -469 flat_params[key] = jsonX.dumps(value) -470 elif value is not None: -471 flat_params[key] = str(value) -472 query_string = urlencode(flat_params) -473 url = f"{url}{'&' if '?' in url else '?'}{query_string}" +450 For **platform OAuth** integrations (``auth_type == "PlatformOauth2"``), +451 a ``Bearer`` token is auto-injected from ``auth.credentials.access_token`` +452 unless an ``Authorization`` header is explicitly provided. +453 +454 Retries up to ``max_retries`` (default 3) on transient network errors +455 with exponential back-off. HTTP 429 responses raise ``RateLimitError`` +456 immediately (no automatic retry). +457 +458 Args: +459 url: The URL to request. +460 method: HTTP method (``"GET"``, ``"POST"``, ``"PUT"``, etc.). +461 params: Query parameters appended to the URL. Nested dicts/lists +462 are JSON-serialized automatically. +463 data: Raw request body. Encoding depends on ``content_type``. +464 json: JSON-serializable payload. Sets ``content_type`` to +465 ``application/json`` automatically. +466 headers: Additional HTTP headers. Merged *after* any auto-injected +467 auth header, so explicit ``Authorization`` and ``User-Agent`` +468 values take precedence. +469 user_agent: Convenience override for the request ``User-Agent``. +470 Ignored when ``headers`` already contains a ``User-Agent`` key. +471 content_type: ``Content-Type`` header value. +472 timeout: Per-request timeout in seconds (overrides ``request_config``). +473 retry_count: Internal — current retry attempt number. 474 -475 # Prepare body -476 if data is not None: -477 if content_type == "application/json": -478 data = jsonX.dumps(data) -479 elif content_type == "application/x-www-form-urlencoded": -480 data = urlencode(data) if isinstance(data, dict) else data -481 -482 # Store the original timeout numeric value -483 original_timeout = timeout or self.config["timeout"] -484 -485 # Convert the numeric timeout to a ClientTimeout instance for this request -486 client_timeout = aiohttp.ClientTimeout(total=original_timeout) -487 -488 try: -489 async with self._session.request( -490 method=method, -491 url=url, -492 data=data, -493 headers=final_headers, -494 timeout=client_timeout, -495 ssl=True -496 ) as response: -497 content_type = response.headers.get("Content-Type", "") -498 -499 if response.status == 429: # Rate limit -500 retry_after = int(response.headers.get("Retry-After", 60)) -501 raise RateLimitError( -502 retry_after, -503 response.status, -504 "Rate limit exceeded", -505 await response.text() -506 ) +475 Returns: +476 A ``FetchResponse`` containing the HTTP status code, response +477 headers, and parsed body data. +478 +479 Raises: +480 RateLimitError: On HTTP 429 with the ``Retry-After`` value. +481 HTTPError: On any other non-2xx status. +482 """ +483 if not self._session: +484 self._session = aiohttp.ClientSession() +485 +486 # Prepare request +487 if json is not None: +488 data = json +489 content_type = "application/json" +490 +491 final_headers = {} +492 +493 if not any(key.lower() == "user-agent" for key in (headers or {})): +494 final_headers["User-Agent"] = user_agent or self._build_default_user_agent() +495 +496 if self.auth and "Authorization" not in (headers or {}): +497 auth_type = AuthType(self.auth.get("auth_type", "PlatformOauth2")) +498 credentials = self.auth.get("credentials", {}) +499 +500 if auth_type == AuthType.PlatformOauth2 and "access_token" in credentials: +501 final_headers["Authorization"] = f"Bearer {credentials['access_token']}" +502 +503 if content_type: +504 final_headers["Content-Type"] = content_type +505 if headers: +506 final_headers.update(headers) 507 -508 try: -509 if "application/json" in content_type: -510 result = await response.json() -511 else: -512 result = await response.text() -513 if not result and response.status in {200, 201, 204}: -514 result = None -515 except Exception as e: -516 self.logger.error(f"Error parsing response: {e}") -517 result = await response.text() +508 if params: +509 # Handle nested dictionary parameters +510 flat_params = {} +511 for key, value in params.items(): +512 if isinstance(value, (dict, list)): +513 flat_params[key] = jsonX.dumps(value) +514 elif value is not None: +515 flat_params[key] = str(value) +516 query_string = urlencode(flat_params) +517 url = f"{url}{'&' if '?' in url else '?'}{query_string}" 518 -519 response_headers = dict(response.headers) -520 -521 if not response.ok: -522 print(f"HTTP error encountered. Status: {response.status}. Result: {result}") -523 raise HTTPError(response.status, str(result), result) -524 -525 return FetchResponse( -526 status=response.status, -527 headers=response_headers, -528 data=result, -529 ) -530 -531 except RateLimitError: -532 raise -533 except (aiohttp.ClientError, asyncio.TimeoutError) as e: -534 # Don't want to send this to Raygun here because this will be retried. -535 print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.") -536 if retry_count < self.config["max_retries"]: -537 await asyncio.sleep(2 ** retry_count) # Exponential backoff -538 print("Retrying request...") -539 # Use original_timeout (numeric) for recursive calls -540 return await self.fetch( -541 url, method, params, data, json, -542 headers, content_type, original_timeout, retry_count + 1 -543 ) -544 else: -545 print("Max retries reached. Raising error.") -546 raise -547 except Exception as e: -548 self.logger.error(f"Unexpected error during {method} {url}: {e}") -549 print(f"Unexpected error encountered: {e}") -550 raise +519 # Prepare body +520 if data is not None: +521 if content_type == "application/json": +522 data = jsonX.dumps(data) +523 elif content_type == "application/x-www-form-urlencoded": +524 data = urlencode(data) if isinstance(data, dict) else data +525 +526 # Store the original timeout numeric value +527 original_timeout = timeout or self.config["timeout"] +528 +529 # Convert the numeric timeout to a ClientTimeout instance for this request +530 client_timeout = aiohttp.ClientTimeout(total=original_timeout) +531 +532 try: +533 async with self._session.request( +534 method=method, +535 url=url, +536 data=data, +537 headers=final_headers, +538 timeout=client_timeout, +539 ssl=True +540 ) as response: +541 content_type = response.headers.get("Content-Type", "") +542 +543 if response.status == 429: # Rate limit +544 retry_after = int(response.headers.get("Retry-After", 60)) +545 raise RateLimitError( +546 retry_after, +547 response.status, +548 "Rate limit exceeded", +549 await response.text() +550 ) +551 +552 try: +553 if "application/json" in content_type: +554 result = await response.json() +555 else: +556 result = await response.text() +557 if not result and response.status in {200, 201, 204}: +558 result = None +559 except Exception as e: +560 self.logger.error(f"Error parsing response: {e}") +561 result = await response.text() +562 +563 response_headers = dict(response.headers) +564 +565 if not response.ok: +566 print(f"HTTP error encountered. Status: {response.status}. Result: {result}") +567 raise HTTPError(response.status, str(result), result) +568 +569 return FetchResponse( +570 status=response.status, +571 headers=response_headers, +572 data=result, +573 ) +574 +575 except RateLimitError: +576 raise +577 except (aiohttp.ClientError, asyncio.TimeoutError) as e: +578 # Don't want to send this to Raygun here because this will be retried. +579 print(f"Error encountered: {e}. Retry count: {retry_count}. Backing off.") +580 if retry_count < self.config["max_retries"]: +581 await asyncio.sleep(2 ** retry_count) # Exponential backoff +582 print("Retrying request...") +583 # Use original_timeout (numeric) for recursive calls +584 return await self.fetch( +585 url, method, params, data, json, +586 headers, content_type, original_timeout, retry_count + 1, +587 user_agent=user_agent, +588 ) +589 else: +590 print("Max retries reached. Raising error.") +591 raise +592 except Exception as e: +593 self.logger.error(f"Unexpected error during {method} {url}: {e}") +594 print(f"Unexpected error encountered: {e}") +595 raiseMake an HTTP request with automatic retries and error handling.
+If no
+User-Agentheader is provided, a default SDKUser-Agentis +added. When the request is made inside a handler executed by +Integration, the integration'sconfig.jsonname and version are +included. Passuser_agentto set a per-request value more easily. +ExplicitUser-Agentheaders always take precedence.For platform OAuth integrations (
@@ -3592,7 +3724,10 @@auth_type == "PlatformOauth2"), aBearertoken is auto-injected fromauth.credentials.access_tokenunless anAuthorizationheader is explicitly provided.Inherited Members
json: JSON-serializable payload. Setscontent_typetoapplication/jsonautomatically. headers: Additional HTTP headers. Merged after any auto-injected - auth header, so an explicitAuthorizationtakes precedence. + auth header, so explicitAuthorizationandUser-Agent+ values take precedence. + user_agent: Convenience override for the requestUser-Agent. + Ignored whenheadersalready contains aUser-Agentkey. content_type:Content-Typeheader value. timeout: Per-request timeout in seconds (overridesrequest_config). retry_count: Internal — current retry attempt number. @@ -3612,7 +3747,7 @@Inherited Members
- + class Integration: @@ -3620,380 +3755,395 @@-Inherited Members
553class Integration: -554 """Base integration class with handler registration and execution. -555 -556 This class manages the integration configuration, handler registration, -557 and provides methods to execute actions and triggers. -558 -559 Args: -560 config: Integration configuration -561 -562 Attributes: -563 config: Integration configuration -564 """ -565 -566 def __init__(self, config: IntegrationConfig): -567 self.config = config -568 """Integration configuration""" -569 self._action_handlers: Dict[str, Type[ActionHandler]] = {} -570 """Action handlers""" -571 self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {} -572 """Polling handlers""" -573 self._connected_account_handler: Optional[Type[ConnectedAccountHandler]] = None -574 """Connected account handler""" -575 -576 @classmethod -577 def load(cls, config_path: Union[str, Path] = None) -> 'Integration': -578 """Load an integration from its ``config.json``. -579 -580 Args: -581 config_path: Explicit path to ``config.json``. When omitted the -582 SDK resolves the path relative to its own package location, -583 which works when the SDK is vendored via -584 ``pip install --target dependencies``. Multi-file integrations -585 that use ``actions/`` sub-packages should pass an explicit path -586 (e.g. ``Integration.load("config.json")``). -587 -588 Returns: -589 A fully initialised ``Integration`` ready for handler registration. -590 -591 Raises: -592 ConfigurationError: If the file is missing or contains invalid JSON. -593 """ -594 if config_path is None: -595 config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json') -596 -597 config_path = Path(config_path) -598 -599 if not config_path.exists(): -600 raise ConfigurationError(f"Configuration file not found: {config_path}") -601 -602 try: -603 with open(config_path, 'r') as f: -604 config_data = json.load(f) -605 except json.JSONDecodeError as e: -606 raise ConfigurationError(f"Invalid JSON configuration: {e}") -607 -608 # Parse configuration sections -609 actions = cls._parse_actions(config_data.get("actions", {})) -610 polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {})) -611 -612 config = IntegrationConfig( -613 name=config_data["name"], -614 version=config_data["version"], -615 description=config_data["description"], -616 auth=config_data.get("auth", {}), -617 actions=actions, -618 polling_triggers=polling_triggers -619 ) +@@ -4013,36 +4163,36 @@598class Integration: +599 """Base integration class with handler registration and execution. +600 +601 This class manages the integration configuration, handler registration, +602 and provides methods to execute actions and triggers. +603 +604 Args: +605 config: Integration configuration +606 +607 Attributes: +608 config: Integration configuration +609 """ +610 +611 def __init__(self, config: IntegrationConfig): +612 self.config = config +613 """Integration configuration""" +614 self._action_handlers: Dict[str, Type[ActionHandler]] = {} +615 """Action handlers""" +616 self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {} +617 """Polling handlers""" +618 self._connected_account_handler: Optional[Type[ConnectedAccountHandler]] = None +619 """Connected account handler""" 620 -621 return cls(config) -622 -623 @staticmethod -624 def _parse_interval(interval_str: str) -> timedelta: -625 """Parse interval string into timedelta""" -626 unit = interval_str[-1].lower() -627 value = int(interval_str[:-1]) -628 -629 if unit == 's': -630 return timedelta(seconds=value) -631 elif unit == 'm': -632 return timedelta(minutes=value) -633 elif unit == 'h': -634 return timedelta(hours=value) -635 elif unit == 'd': -636 return timedelta(days=value) -637 else: -638 raise ConfigurationError(f"Invalid interval format: {interval_str}") -639 -640 @classmethod -641 def _parse_actions(cls, actions_config: Dict[str, Any]) -> Dict[str, Action]: -642 """Parse action configurations""" -643 actions = {} -644 for name, data in actions_config.items(): -645 actions[name] = Action( -646 name=name, -647 description=data["description"], -648 input_schema=data["input_schema"], -649 output_schema=data["output_schema"] -650 ) -651 -652 return actions -653 -654 @classmethod -655 def _parse_polling_triggers(cls, triggers_config: Dict[str, Any]) -> Dict[str, PollingTrigger]: -656 """Parse polling trigger configurations""" -657 triggers = {} -658 for name, data in triggers_config.items(): -659 interval = cls._parse_interval(data["polling_interval"]) -660 -661 triggers[name] = PollingTrigger( -662 name=name, -663 description=data["description"], -664 polling_interval=interval, -665 input_schema=data["input_schema"], -666 output_schema=data["output_schema"] -667 ) -668 -669 return triggers -670 -671 def action(self, name: str): -672 """Decorator to register an action handler. -673 -674 Args: -675 name: Name of the action to register -676 -677 Returns: -678 Decorator function -679 -680 Raises: -681 ConfigurationError: If action is not defined in config -682 -683 Example: -684 ```python -685 @integration.action("my_action") -686 class MyActionHandler(ActionHandler): -687 async def execute(self, inputs, context): -688 # Implementation -689 return result -690 ``` -691 """ -692 def decorator(handler_class: Type[ActionHandler]): -693 if name not in self.config.actions: -694 raise ConfigurationError(f"Action '{name}' not defined in config") -695 self._action_handlers[name] = handler_class -696 return handler_class -697 return decorator +621 @classmethod +622 def load(cls, config_path: Union[str, Path] = None) -> 'Integration': +623 """Load an integration from its ``config.json``. +624 +625 Args: +626 config_path: Explicit path to ``config.json``. When omitted the +627 SDK resolves the path relative to its own package location, +628 which works when the SDK is vendored via +629 ``pip install --target dependencies``. Multi-file integrations +630 that use ``actions/`` sub-packages should pass an explicit path +631 (e.g. ``Integration.load("config.json")``). +632 +633 Returns: +634 A fully initialised ``Integration`` ready for handler registration. +635 +636 Raises: +637 ConfigurationError: If the file is missing or contains invalid JSON. +638 """ +639 if config_path is None: +640 config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json') +641 +642 config_path = Path(config_path) +643 +644 if not config_path.exists(): +645 raise ConfigurationError(f"Configuration file not found: {config_path}") +646 +647 try: +648 with open(config_path, 'r') as f: +649 config_data = json.load(f) +650 except json.JSONDecodeError as e: +651 raise ConfigurationError(f"Invalid JSON configuration: {e}") +652 +653 # Parse configuration sections +654 actions = cls._parse_actions(config_data.get("actions", {})) +655 polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {})) +656 +657 config = IntegrationConfig( +658 name=config_data["name"], +659 version=config_data["version"], +660 description=config_data["description"], +661 auth=config_data.get("auth", {}), +662 actions=actions, +663 polling_triggers=polling_triggers +664 ) +665 +666 return cls(config) +667 +668 @staticmethod +669 def _parse_interval(interval_str: str) -> timedelta: +670 """Parse interval string into timedelta""" +671 unit = interval_str[-1].lower() +672 value = int(interval_str[:-1]) +673 +674 if unit == 's': +675 return timedelta(seconds=value) +676 elif unit == 'm': +677 return timedelta(minutes=value) +678 elif unit == 'h': +679 return timedelta(hours=value) +680 elif unit == 'd': +681 return timedelta(days=value) +682 else: +683 raise ConfigurationError(f"Invalid interval format: {interval_str}") +684 +685 @classmethod +686 def _parse_actions(cls, actions_config: Dict[str, Any]) -> Dict[str, Action]: +687 """Parse action configurations""" +688 actions = {} +689 for name, data in actions_config.items(): +690 actions[name] = Action( +691 name=name, +692 description=data["description"], +693 input_schema=data["input_schema"], +694 output_schema=data["output_schema"] +695 ) +696 +697 return actions 698 -699 def polling_trigger(self, name: str): -700 """Decorator to register a polling trigger handler -701 -702 Args: -703 name: Name of the polling trigger to register -704 -705 Returns: -706 Decorator function -707 -708 Raises: -709 ConfigurationError: If polling trigger is not defined in config -710 -711 Example: -712 ```python -713 @integration.polling_trigger("my_polling_trigger") -714 class MyPollingTriggerHandler(PollingTriggerHandler): -715 async def poll(self, inputs, last_poll_ts, context): -716 # Implementation -717 return result -718 ``` -719 """ -720 def decorator(handler_class: Type[PollingTriggerHandler]): -721 if name not in self.config.polling_triggers: -722 raise ConfigurationError(f"Polling trigger '{name}' not defined in config") -723 self._polling_handlers[name] = handler_class -724 return handler_class -725 return decorator -726 -727 def connected_account(self): -728 """Decorator to register a connected account handler -729 -730 Returns: -731 Decorator function -732 -733 Example: -734 ```python -735 @integration.connected_account() -736 class MyConnectedAccountHandler(ConnectedAccountHandler): -737 async def get_account_info(self, context): -738 # Implementation -739 return {"email": "user@example.com", "name": "John Doe"} -740 ``` -741 """ -742 def decorator(handler_class: Type[ConnectedAccountHandler]): -743 self._connected_account_handler = handler_class -744 return handler_class -745 return decorator -746 -747 async def execute_action(self, -748 name: str, -749 inputs: Dict[str, Any], -750 context: ExecutionContext) -> IntegrationResult: -751 """Execute a registered action. -752 -753 Args: -754 name: Name of the action to execute -755 inputs: Action inputs -756 context: Execution context -757 -758 Returns: -759 IntegrationResult with action data (ResultType.ACTION), -760 action error (ResultType.ACTION_ERROR) if the handler returned ActionError, -761 or validation error (ResultType.VALIDATION_ERROR) if schema validation fails. -762 """ -763 try: -764 if name not in self._action_handlers: -765 raise ValidationError(f"Action '{name}' not registered") -766 -767 # Validate inputs against action schema -768 action_config = self.config.actions[name] -769 validator = Draft7Validator(action_config.input_schema) -770 errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path) -771 if errors: -772 message = "" -773 for error in errors: -774 message += f"{list(error.schema_path)}, {error.message},\n " -775 raise ValidationError(message, action_config.input_schema, inputs, source="input") -776 -777 if "fields" in self.config.auth: -778 auth_config = self.config.auth["fields"] -779 validator = Draft7Validator(auth_config) -780 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) -781 if errors: -782 message = "" -783 for error in errors: -784 message += f"{list(error.schema_path)}, {error.message},\n " -785 raise ValidationError(message, auth_config, context.auth, source="input") -786 -787 # Create handler instance and execute -788 handler = self._action_handlers[name]() -789 result = await handler.execute(inputs, context) -790 -791 # Handle ActionError - skip output schema validation -792 if isinstance(result, ActionError): -793 return IntegrationResult( -794 version=__version__, -795 type=ResultType.ACTION_ERROR, -796 result=result -797 ) -798 -799 # Validate that result is ActionResult -800 if not isinstance(result, ActionResult): -801 raise ValidationError( -802 f"Action handler '{name}' must return ActionResult or ActionError, got {type(result).__name__}", -803 source="output" -804 ) -805 -806 # Validate output schema against the data inside ActionResult -807 validator = Draft7Validator(action_config.output_schema) -808 errors = sorted(validator.iter_errors(result.data), key=lambda e: e.path) -809 if errors: -810 message = "" -811 for error in errors: -812 message += f"{list(error.schema_path)}, {error.message},\n " -813 raise ValidationError(message, action_config.output_schema, result.data, source="output") -814 -815 # Return IntegrationResult with ActionResult directly -816 return IntegrationResult( -817 version=__version__, -818 type=ResultType.ACTION, -819 result=result -820 ) -821 except ValidationError as e: -822 return IntegrationResult( -823 version=__version__, -824 type=ResultType.VALIDATION_ERROR, -825 result={ -826 'message': str(e), -827 'property': None, -828 'value': None, -829 'source': getattr(e, 'source', 'legacy') -830 } -831 ) -832 -833 async def execute_polling_trigger(self, -834 name: str, -835 inputs: Dict[str, Any], -836 last_poll_ts: Optional[str], -837 context: ExecutionContext) -> List[Dict[str, Any]]: -838 """Execute a registered polling trigger -839 -840 Args: -841 name: Name of the polling trigger to execute -842 inputs: Trigger inputs -843 last_poll_ts: Last poll timestamp -844 context: Execution context -845 -846 Returns: -847 List of records -848 -849 Raises: -850 ValidationError: If inputs or outputs don't match schema -851 """ -852 if name not in self._polling_handlers: -853 raise ValidationError(f"Polling trigger '{name}' not registered") -854 -855 # Validate trigger configuration -856 trigger_config = self.config.polling_triggers[name] -857 try: -858 validate(inputs, trigger_config.input_schema) -859 except Exception as e: -860 raise ValidationError(e.message, e.schema, e.instance) -861 -862 try: -863 auth_config = self.config.auth["fields"] -864 validate(context.auth, auth_config) -865 except Exception as e: -866 raise ValidationError(e.message, e.schema, e.instance) -867 -868 # Create handler instance and execute -869 handler = self._polling_handlers[name]() -870 records = await handler.poll(inputs, last_poll_ts, context) -871 # Validate each record -872 for record in records: -873 if "id" not in record: -874 raise ValidationError( -875 f"Polling trigger '{name}' returned record without required 'id' field") -876 if "data" not in record: -877 raise ValidationError( -878 f"Polling trigger '{name}' returned record without required 'data' field") -879 -880 # Validate record data against output schema -881 try: -882 validate(record["data"], trigger_config.output_schema) -883 except Exception as e: -884 raise ValidationError(e.message, e.schema, e.instance) -885 -886 return records -887 -888 async def get_connected_account(self, context: ExecutionContext) -> IntegrationResult: -889 """Get connected account information -890 -891 Args: -892 context: Execution context -893 -894 Returns: -895 IntegrationResult containing connected account data -896 -897 Raises: -898 ValidationError: If no connected account handler is registered or auth is invalid -899 """ -900 if not self._connected_account_handler: -901 raise ValidationError("No connected account handler registered") -902 -903 if "fields" in self.config.auth: -904 auth_config = self.config.auth["fields"] -905 validator = Draft7Validator(auth_config) -906 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) -907 if errors: -908 message = "" -909 for error in errors: -910 message += f"{list(error.schema_path)}, {error.message},\n " -911 raise ValidationError(message, auth_config, context.auth) -912 -913 handler = self._connected_account_handler() -914 account_info = await handler.get_account_info(context) -915 -916 if not isinstance(account_info, ConnectedAccountInfo): -917 raise ValidationError( -918 f"Connected account handler must return ConnectedAccountInfo, got {type(account_info).__name__}" -919 ) -920 -921 # Return IntegrationResult with ConnectedAccountInfo object directly -922 return IntegrationResult( -923 version=__version__, -924 type=ResultType.CONNECTED_ACCOUNT, -925 result=account_info -926 ) +699 @classmethod +700 def _parse_polling_triggers(cls, triggers_config: Dict[str, Any]) -> Dict[str, PollingTrigger]: +701 """Parse polling trigger configurations""" +702 triggers = {} +703 for name, data in triggers_config.items(): +704 interval = cls._parse_interval(data["polling_interval"]) +705 +706 triggers[name] = PollingTrigger( +707 name=name, +708 description=data["description"], +709 polling_interval=interval, +710 input_schema=data["input_schema"], +711 output_schema=data["output_schema"] +712 ) +713 +714 return triggers +715 +716 def action(self, name: str): +717 """Decorator to register an action handler. +718 +719 Args: +720 name: Name of the action to register +721 +722 Returns: +723 Decorator function +724 +725 Raises: +726 ConfigurationError: If action is not defined in config +727 +728 Example: +729 ```python +730 @integration.action("my_action") +731 class MyActionHandler(ActionHandler): +732 async def execute(self, inputs, context): +733 # Implementation +734 return result +735 ``` +736 """ +737 def decorator(handler_class: Type[ActionHandler]): +738 if name not in self.config.actions: +739 raise ConfigurationError(f"Action '{name}' not defined in config") +740 self._action_handlers[name] = handler_class +741 return handler_class +742 return decorator +743 +744 def polling_trigger(self, name: str): +745 """Decorator to register a polling trigger handler +746 +747 Args: +748 name: Name of the polling trigger to register +749 +750 Returns: +751 Decorator function +752 +753 Raises: +754 ConfigurationError: If polling trigger is not defined in config +755 +756 Example: +757 ```python +758 @integration.polling_trigger("my_polling_trigger") +759 class MyPollingTriggerHandler(PollingTriggerHandler): +760 async def poll(self, inputs, last_poll_ts, context): +761 # Implementation +762 return result +763 ``` +764 """ +765 def decorator(handler_class: Type[PollingTriggerHandler]): +766 if name not in self.config.polling_triggers: +767 raise ConfigurationError(f"Polling trigger '{name}' not defined in config") +768 self._polling_handlers[name] = handler_class +769 return handler_class +770 return decorator +771 +772 def connected_account(self): +773 """Decorator to register a connected account handler +774 +775 Returns: +776 Decorator function +777 +778 Example: +779 ```python +780 @integration.connected_account() +781 class MyConnectedAccountHandler(ConnectedAccountHandler): +782 async def get_account_info(self, context): +783 # Implementation +784 return {"email": "user@example.com", "name": "John Doe"} +785 ``` +786 """ +787 def decorator(handler_class: Type[ConnectedAccountHandler]): +788 self._connected_account_handler = handler_class +789 return handler_class +790 return decorator +791 +792 async def execute_action(self, +793 name: str, +794 inputs: Dict[str, Any], +795 context: ExecutionContext) -> IntegrationResult: +796 """Execute a registered action. +797 +798 Args: +799 name: Name of the action to execute +800 inputs: Action inputs +801 context: Execution context +802 +803 Returns: +804 IntegrationResult with action data (ResultType.ACTION), +805 action error (ResultType.ACTION_ERROR) if the handler returned ActionError, +806 or validation error (ResultType.VALIDATION_ERROR) if schema validation fails. +807 """ +808 try: +809 if name not in self._action_handlers: +810 raise ValidationError(f"Action '{name}' not registered") +811 +812 # Validate inputs against action schema +813 action_config = self.config.actions[name] +814 validator = Draft7Validator(action_config.input_schema) +815 errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path) +816 if errors: +817 message = "" +818 for error in errors: +819 message += f"{list(error.schema_path)}, {error.message},\n " +820 raise ValidationError(message, action_config.input_schema, inputs, source="input") +821 +822 if "fields" in self.config.auth: +823 auth_config = self.config.auth["fields"] +824 validator = Draft7Validator(auth_config) +825 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) +826 if errors: +827 message = "" +828 for error in errors: +829 message += f"{list(error.schema_path)}, {error.message},\n " +830 raise ValidationError(message, auth_config, context.auth, source="input") +831 +832 # Create handler instance and execute +833 handler = self._action_handlers[name]() +834 previous_identity = (context._integration_name, context._integration_version) +835 context._set_integration_identity(self.config.name, self.config.version) +836 try: +837 result = await handler.execute(inputs, context) +838 finally: +839 context._set_integration_identity(*previous_identity) +840 +841 # Handle ActionError - skip output schema validation +842 if isinstance(result, ActionError): +843 return IntegrationResult( +844 version=__version__, +845 type=ResultType.ACTION_ERROR, +846 result=result +847 ) +848 +849 # Validate that result is ActionResult +850 if not isinstance(result, ActionResult): +851 raise ValidationError( +852 f"Action handler '{name}' must return ActionResult or ActionError, got {type(result).__name__}", +853 source="output" +854 ) +855 +856 # Validate output schema against the data inside ActionResult +857 validator = Draft7Validator(action_config.output_schema) +858 errors = sorted(validator.iter_errors(result.data), key=lambda e: e.path) +859 if errors: +860 message = "" +861 for error in errors: +862 message += f"{list(error.schema_path)}, {error.message},\n " +863 raise ValidationError(message, action_config.output_schema, result.data, source="output") +864 +865 # Return IntegrationResult with ActionResult directly +866 return IntegrationResult( +867 version=__version__, +868 type=ResultType.ACTION, +869 result=result +870 ) +871 except ValidationError as e: +872 return IntegrationResult( +873 version=__version__, +874 type=ResultType.VALIDATION_ERROR, +875 result={ +876 'message': str(e), +877 'property': None, +878 'value': None, +879 'source': getattr(e, 'source', 'legacy') +880 } +881 ) +882 +883 async def execute_polling_trigger(self, +884 name: str, +885 inputs: Dict[str, Any], +886 last_poll_ts: Optional[str], +887 context: ExecutionContext) -> List[Dict[str, Any]]: +888 """Execute a registered polling trigger +889 +890 Args: +891 name: Name of the polling trigger to execute +892 inputs: Trigger inputs +893 last_poll_ts: Last poll timestamp +894 context: Execution context +895 +896 Returns: +897 List of records +898 +899 Raises: +900 ValidationError: If inputs or outputs don't match schema +901 """ +902 if name not in self._polling_handlers: +903 raise ValidationError(f"Polling trigger '{name}' not registered") +904 +905 # Validate trigger configuration +906 trigger_config = self.config.polling_triggers[name] +907 try: +908 validate(inputs, trigger_config.input_schema) +909 except Exception as e: +910 raise ValidationError(e.message, e.schema, e.instance) +911 +912 try: +913 auth_config = self.config.auth["fields"] +914 validate(context.auth, auth_config) +915 except Exception as e: +916 raise ValidationError(e.message, e.schema, e.instance) +917 +918 # Create handler instance and execute +919 handler = self._polling_handlers[name]() +920 previous_identity = (context._integration_name, context._integration_version) +921 context._set_integration_identity(self.config.name, self.config.version) +922 try: +923 records = await handler.poll(inputs, last_poll_ts, context) +924 finally: +925 context._set_integration_identity(*previous_identity) +926 # Validate each record +927 for record in records: +928 if "id" not in record: +929 raise ValidationError( +930 f"Polling trigger '{name}' returned record without required 'id' field") +931 if "data" not in record: +932 raise ValidationError( +933 f"Polling trigger '{name}' returned record without required 'data' field") +934 +935 # Validate record data against output schema +936 try: +937 validate(record["data"], trigger_config.output_schema) +938 except Exception as e: +939 raise ValidationError(e.message, e.schema, e.instance) +940 +941 return records +942 +943 async def get_connected_account(self, context: ExecutionContext) -> IntegrationResult: +944 """Get connected account information +945 +946 Args: +947 context: Execution context +948 +949 Returns: +950 IntegrationResult containing connected account data +951 +952 Raises: +953 ValidationError: If no connected account handler is registered or auth is invalid +954 """ +955 if not self._connected_account_handler: +956 raise ValidationError("No connected account handler registered") +957 +958 if "fields" in self.config.auth: +959 auth_config = self.config.auth["fields"] +960 validator = Draft7Validator(auth_config) +961 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) +962 if errors: +963 message = "" +964 for error in errors: +965 message += f"{list(error.schema_path)}, {error.message},\n " +966 raise ValidationError(message, auth_config, context.auth) +967 +968 handler = self._connected_account_handler() +969 previous_identity = (context._integration_name, context._integration_version) +970 context._set_integration_identity(self.config.name, self.config.version) +971 try: +972 account_info = await handler.get_account_info(context) +973 finally: +974 context._set_integration_identity(*previous_identity) +975 +976 if not isinstance(account_info, ConnectedAccountInfo): +977 raise ValidationError( +978 f"Connected account handler must return ConnectedAccountInfo, got {type(account_info).__name__}" +979 ) +980 +981 # Return IntegrationResult with ConnectedAccountInfo object directly +982 return IntegrationResult( +983 version=__version__, +984 type=ResultType.CONNECTED_ACCOUNT, +985 result=account_info +986 )Inherited Members
- + Integration(config: IntegrationConfig)--566 def __init__(self, config: IntegrationConfig): -567 self.config = config -568 """Integration configuration""" -569 self._action_handlers: Dict[str, Type[ActionHandler]] = {} -570 """Action handlers""" -571 self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {} -572 """Polling handlers""" -573 self._connected_account_handler: Optional[Type[ConnectedAccountHandler]] = None -574 """Connected account handler""" +- +611 def __init__(self, config: IntegrationConfig): +612 self.config = config +613 """Integration configuration""" +614 self._action_handlers: Dict[str, Type[ActionHandler]] = {} +615 """Action handlers""" +616 self._polling_handlers: Dict[str, Type[PollingTriggerHandler]] = {} +617 """Polling handlers""" +618 self._connected_account_handler: Optional[Type[ConnectedAccountHandler]] = None +619 """Connected account handler"""576 @classmethod -577 def load(cls, config_path: Union[str, Path] = None) -> 'Integration': -578 """Load an integration from its ``config.json``. -579 -580 Args: -581 config_path: Explicit path to ``config.json``. When omitted the -582 SDK resolves the path relative to its own package location, -583 which works when the SDK is vendored via -584 ``pip install --target dependencies``. Multi-file integrations -585 that use ``actions/`` sub-packages should pass an explicit path -586 (e.g. ``Integration.load("config.json")``). -587 -588 Returns: -589 A fully initialised ``Integration`` ready for handler registration. -590 -591 Raises: -592 ConfigurationError: If the file is missing or contains invalid JSON. -593 """ -594 if config_path is None: -595 config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json') -596 -597 config_path = Path(config_path) -598 -599 if not config_path.exists(): -600 raise ConfigurationError(f"Configuration file not found: {config_path}") -601 -602 try: -603 with open(config_path, 'r') as f: -604 config_data = json.load(f) -605 except json.JSONDecodeError as e: -606 raise ConfigurationError(f"Invalid JSON configuration: {e}") -607 -608 # Parse configuration sections -609 actions = cls._parse_actions(config_data.get("actions", {})) -610 polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {})) -611 -612 config = IntegrationConfig( -613 name=config_data["name"], -614 version=config_data["version"], -615 description=config_data["description"], -616 auth=config_data.get("auth", {}), -617 actions=actions, -618 polling_triggers=polling_triggers -619 ) -620 -621 return cls(config) +@@ -4131,7 +4281,7 @@621 @classmethod +622 def load(cls, config_path: Union[str, Path] = None) -> 'Integration': +623 """Load an integration from its ``config.json``. +624 +625 Args: +626 config_path: Explicit path to ``config.json``. When omitted the +627 SDK resolves the path relative to its own package location, +628 which works when the SDK is vendored via +629 ``pip install --target dependencies``. Multi-file integrations +630 that use ``actions/`` sub-packages should pass an explicit path +631 (e.g. ``Integration.load("config.json")``). +632 +633 Returns: +634 A fully initialised ``Integration`` ready for handler registration. +635 +636 Raises: +637 ConfigurationError: If the file is missing or contains invalid JSON. +638 """ +639 if config_path is None: +640 config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.json') +641 +642 config_path = Path(config_path) +643 +644 if not config_path.exists(): +645 raise ConfigurationError(f"Configuration file not found: {config_path}") +646 +647 try: +648 with open(config_path, 'r') as f: +649 config_data = json.load(f) +650 except json.JSONDecodeError as e: +651 raise ConfigurationError(f"Invalid JSON configuration: {e}") +652 +653 # Parse configuration sections +654 actions = cls._parse_actions(config_data.get("actions", {})) +655 polling_triggers = cls._parse_polling_triggers(config_data.get("polling_triggers", {})) +656 +657 config = IntegrationConfig( +658 name=config_data["name"], +659 version=config_data["version"], +660 description=config_data["description"], +661 auth=config_data.get("auth", {}), +662 actions=actions, +663 polling_triggers=polling_triggers +664 ) +665 +666 return cls(config)Inherited Members
- + def action(self, name: str): @@ -4139,33 +4289,33 @@-Inherited Members
671 def action(self, name: str): -672 """Decorator to register an action handler. -673 -674 Args: -675 name: Name of the action to register -676 -677 Returns: -678 Decorator function -679 -680 Raises: -681 ConfigurationError: If action is not defined in config -682 -683 Example: -684 ```python -685 @integration.action("my_action") -686 class MyActionHandler(ActionHandler): -687 async def execute(self, inputs, context): -688 # Implementation -689 return result -690 ``` -691 """ -692 def decorator(handler_class: Type[ActionHandler]): -693 if name not in self.config.actions: -694 raise ConfigurationError(f"Action '{name}' not defined in config") -695 self._action_handlers[name] = handler_class -696 return handler_class -697 return decorator +@@ -4198,7 +4348,7 @@716 def action(self, name: str): +717 """Decorator to register an action handler. +718 +719 Args: +720 name: Name of the action to register +721 +722 Returns: +723 Decorator function +724 +725 Raises: +726 ConfigurationError: If action is not defined in config +727 +728 Example: +729 ```python +730 @integration.action("my_action") +731 class MyActionHandler(ActionHandler): +732 async def execute(self, inputs, context): +733 # Implementation +734 return result +735 ``` +736 """ +737 def decorator(handler_class: Type[ActionHandler]): +738 if name not in self.config.actions: +739 raise ConfigurationError(f"Action '{name}' not defined in config") +740 self._action_handlers[name] = handler_class +741 return handler_class +742 return decoratorInherited Members
- + def polling_trigger(self, name: str): @@ -4206,33 +4356,33 @@-Inherited Members
699 def polling_trigger(self, name: str): -700 """Decorator to register a polling trigger handler -701 -702 Args: -703 name: Name of the polling trigger to register -704 -705 Returns: -706 Decorator function -707 -708 Raises: -709 ConfigurationError: If polling trigger is not defined in config -710 -711 Example: -712 ```python -713 @integration.polling_trigger("my_polling_trigger") -714 class MyPollingTriggerHandler(PollingTriggerHandler): -715 async def poll(self, inputs, last_poll_ts, context): -716 # Implementation -717 return result -718 ``` -719 """ -720 def decorator(handler_class: Type[PollingTriggerHandler]): -721 if name not in self.config.polling_triggers: -722 raise ConfigurationError(f"Polling trigger '{name}' not defined in config") -723 self._polling_handlers[name] = handler_class -724 return handler_class -725 return decorator +@@ -4265,7 +4415,7 @@744 def polling_trigger(self, name: str): +745 """Decorator to register a polling trigger handler +746 +747 Args: +748 name: Name of the polling trigger to register +749 +750 Returns: +751 Decorator function +752 +753 Raises: +754 ConfigurationError: If polling trigger is not defined in config +755 +756 Example: +757 ```python +758 @integration.polling_trigger("my_polling_trigger") +759 class MyPollingTriggerHandler(PollingTriggerHandler): +760 async def poll(self, inputs, last_poll_ts, context): +761 # Implementation +762 return result +763 ``` +764 """ +765 def decorator(handler_class: Type[PollingTriggerHandler]): +766 if name not in self.config.polling_triggers: +767 raise ConfigurationError(f"Polling trigger '{name}' not defined in config") +768 self._polling_handlers[name] = handler_class +769 return handler_class +770 return decoratorInherited Members
- + def connected_account(self): @@ -4273,25 +4423,25 @@-Inherited Members
727 def connected_account(self): -728 """Decorator to register a connected account handler -729 -730 Returns: -731 Decorator function -732 -733 Example: -734 ```python -735 @integration.connected_account() -736 class MyConnectedAccountHandler(ConnectedAccountHandler): -737 async def get_account_info(self, context): -738 # Implementation -739 return {"email": "user@example.com", "name": "John Doe"} -740 ``` -741 """ -742 def decorator(handler_class: Type[ConnectedAccountHandler]): -743 self._connected_account_handler = handler_class -744 return handler_class -745 return decorator +@@ -4318,7 +4468,7 @@772 def connected_account(self): +773 """Decorator to register a connected account handler +774 +775 Returns: +776 Decorator function +777 +778 Example: +779 ```python +780 @integration.connected_account() +781 class MyConnectedAccountHandler(ConnectedAccountHandler): +782 async def get_account_info(self, context): +783 # Implementation +784 return {"email": "user@example.com", "name": "John Doe"} +785 ``` +786 """ +787 def decorator(handler_class: Type[ConnectedAccountHandler]): +788 self._connected_account_handler = handler_class +789 return handler_class +790 return decoratorInherited Members
- + async def execute_action( self, name: str, inputs: Dict[str, Any], context: ExecutionContext) -> IntegrationResult: @@ -4326,91 +4476,96 @@-Inherited Members
747 async def execute_action(self, -748 name: str, -749 inputs: Dict[str, Any], -750 context: ExecutionContext) -> IntegrationResult: -751 """Execute a registered action. -752 -753 Args: -754 name: Name of the action to execute -755 inputs: Action inputs -756 context: Execution context -757 -758 Returns: -759 IntegrationResult with action data (ResultType.ACTION), -760 action error (ResultType.ACTION_ERROR) if the handler returned ActionError, -761 or validation error (ResultType.VALIDATION_ERROR) if schema validation fails. -762 """ -763 try: -764 if name not in self._action_handlers: -765 raise ValidationError(f"Action '{name}' not registered") -766 -767 # Validate inputs against action schema -768 action_config = self.config.actions[name] -769 validator = Draft7Validator(action_config.input_schema) -770 errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path) -771 if errors: -772 message = "" -773 for error in errors: -774 message += f"{list(error.schema_path)}, {error.message},\n " -775 raise ValidationError(message, action_config.input_schema, inputs, source="input") -776 -777 if "fields" in self.config.auth: -778 auth_config = self.config.auth["fields"] -779 validator = Draft7Validator(auth_config) -780 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) -781 if errors: -782 message = "" -783 for error in errors: -784 message += f"{list(error.schema_path)}, {error.message},\n " -785 raise ValidationError(message, auth_config, context.auth, source="input") -786 -787 # Create handler instance and execute -788 handler = self._action_handlers[name]() -789 result = await handler.execute(inputs, context) -790 -791 # Handle ActionError - skip output schema validation -792 if isinstance(result, ActionError): -793 return IntegrationResult( -794 version=__version__, -795 type=ResultType.ACTION_ERROR, -796 result=result -797 ) -798 -799 # Validate that result is ActionResult -800 if not isinstance(result, ActionResult): -801 raise ValidationError( -802 f"Action handler '{name}' must return ActionResult or ActionError, got {type(result).__name__}", -803 source="output" -804 ) -805 -806 # Validate output schema against the data inside ActionResult -807 validator = Draft7Validator(action_config.output_schema) -808 errors = sorted(validator.iter_errors(result.data), key=lambda e: e.path) -809 if errors: -810 message = "" -811 for error in errors: -812 message += f"{list(error.schema_path)}, {error.message},\n " -813 raise ValidationError(message, action_config.output_schema, result.data, source="output") -814 -815 # Return IntegrationResult with ActionResult directly -816 return IntegrationResult( -817 version=__version__, -818 type=ResultType.ACTION, -819 result=result -820 ) -821 except ValidationError as e: -822 return IntegrationResult( -823 version=__version__, -824 type=ResultType.VALIDATION_ERROR, -825 result={ -826 'message': str(e), -827 'property': None, -828 'value': None, -829 'source': getattr(e, 'source', 'legacy') -830 } -831 ) +@@ -4432,7 +4587,7 @@792 async def execute_action(self, +793 name: str, +794 inputs: Dict[str, Any], +795 context: ExecutionContext) -> IntegrationResult: +796 """Execute a registered action. +797 +798 Args: +799 name: Name of the action to execute +800 inputs: Action inputs +801 context: Execution context +802 +803 Returns: +804 IntegrationResult with action data (ResultType.ACTION), +805 action error (ResultType.ACTION_ERROR) if the handler returned ActionError, +806 or validation error (ResultType.VALIDATION_ERROR) if schema validation fails. +807 """ +808 try: +809 if name not in self._action_handlers: +810 raise ValidationError(f"Action '{name}' not registered") +811 +812 # Validate inputs against action schema +813 action_config = self.config.actions[name] +814 validator = Draft7Validator(action_config.input_schema) +815 errors = sorted(validator.iter_errors(inputs), key=lambda e: e.path) +816 if errors: +817 message = "" +818 for error in errors: +819 message += f"{list(error.schema_path)}, {error.message},\n " +820 raise ValidationError(message, action_config.input_schema, inputs, source="input") +821 +822 if "fields" in self.config.auth: +823 auth_config = self.config.auth["fields"] +824 validator = Draft7Validator(auth_config) +825 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) +826 if errors: +827 message = "" +828 for error in errors: +829 message += f"{list(error.schema_path)}, {error.message},\n " +830 raise ValidationError(message, auth_config, context.auth, source="input") +831 +832 # Create handler instance and execute +833 handler = self._action_handlers[name]() +834 previous_identity = (context._integration_name, context._integration_version) +835 context._set_integration_identity(self.config.name, self.config.version) +836 try: +837 result = await handler.execute(inputs, context) +838 finally: +839 context._set_integration_identity(*previous_identity) +840 +841 # Handle ActionError - skip output schema validation +842 if isinstance(result, ActionError): +843 return IntegrationResult( +844 version=__version__, +845 type=ResultType.ACTION_ERROR, +846 result=result +847 ) +848 +849 # Validate that result is ActionResult +850 if not isinstance(result, ActionResult): +851 raise ValidationError( +852 f"Action handler '{name}' must return ActionResult or ActionError, got {type(result).__name__}", +853 source="output" +854 ) +855 +856 # Validate output schema against the data inside ActionResult +857 validator = Draft7Validator(action_config.output_schema) +858 errors = sorted(validator.iter_errors(result.data), key=lambda e: e.path) +859 if errors: +860 message = "" +861 for error in errors: +862 message += f"{list(error.schema_path)}, {error.message},\n " +863 raise ValidationError(message, action_config.output_schema, result.data, source="output") +864 +865 # Return IntegrationResult with ActionResult directly +866 return IntegrationResult( +867 version=__version__, +868 type=ResultType.ACTION, +869 result=result +870 ) +871 except ValidationError as e: +872 return IntegrationResult( +873 version=__version__, +874 type=ResultType.VALIDATION_ERROR, +875 result={ +876 'message': str(e), +877 'property': None, +878 'value': None, +879 'source': getattr(e, 'source', 'legacy') +880 } +881 )Inherited Members
- + async def execute_polling_trigger( self, name: str, inputs: Dict[str, Any], last_poll_ts: Optional[str], context: ExecutionContext) -> List[Dict[str, Any]]: @@ -4440,60 +4595,65 @@-Inherited Members
833 async def execute_polling_trigger(self, -834 name: str, -835 inputs: Dict[str, Any], -836 last_poll_ts: Optional[str], -837 context: ExecutionContext) -> List[Dict[str, Any]]: -838 """Execute a registered polling trigger -839 -840 Args: -841 name: Name of the polling trigger to execute -842 inputs: Trigger inputs -843 last_poll_ts: Last poll timestamp -844 context: Execution context -845 -846 Returns: -847 List of records -848 -849 Raises: -850 ValidationError: If inputs or outputs don't match schema -851 """ -852 if name not in self._polling_handlers: -853 raise ValidationError(f"Polling trigger '{name}' not registered") -854 -855 # Validate trigger configuration -856 trigger_config = self.config.polling_triggers[name] -857 try: -858 validate(inputs, trigger_config.input_schema) -859 except Exception as e: -860 raise ValidationError(e.message, e.schema, e.instance) -861 -862 try: -863 auth_config = self.config.auth["fields"] -864 validate(context.auth, auth_config) -865 except Exception as e: -866 raise ValidationError(e.message, e.schema, e.instance) -867 -868 # Create handler instance and execute -869 handler = self._polling_handlers[name]() -870 records = await handler.poll(inputs, last_poll_ts, context) -871 # Validate each record -872 for record in records: -873 if "id" not in record: -874 raise ValidationError( -875 f"Polling trigger '{name}' returned record without required 'id' field") -876 if "data" not in record: -877 raise ValidationError( -878 f"Polling trigger '{name}' returned record without required 'data' field") -879 -880 # Validate record data against output schema -881 try: -882 validate(record["data"], trigger_config.output_schema) -883 except Exception as e: -884 raise ValidationError(e.message, e.schema, e.instance) -885 -886 return records +@@ -4517,7 +4677,7 @@883 async def execute_polling_trigger(self, +884 name: str, +885 inputs: Dict[str, Any], +886 last_poll_ts: Optional[str], +887 context: ExecutionContext) -> List[Dict[str, Any]]: +888 """Execute a registered polling trigger +889 +890 Args: +891 name: Name of the polling trigger to execute +892 inputs: Trigger inputs +893 last_poll_ts: Last poll timestamp +894 context: Execution context +895 +896 Returns: +897 List of records +898 +899 Raises: +900 ValidationError: If inputs or outputs don't match schema +901 """ +902 if name not in self._polling_handlers: +903 raise ValidationError(f"Polling trigger '{name}' not registered") +904 +905 # Validate trigger configuration +906 trigger_config = self.config.polling_triggers[name] +907 try: +908 validate(inputs, trigger_config.input_schema) +909 except Exception as e: +910 raise ValidationError(e.message, e.schema, e.instance) +911 +912 try: +913 auth_config = self.config.auth["fields"] +914 validate(context.auth, auth_config) +915 except Exception as e: +916 raise ValidationError(e.message, e.schema, e.instance) +917 +918 # Create handler instance and execute +919 handler = self._polling_handlers[name]() +920 previous_identity = (context._integration_name, context._integration_version) +921 context._set_integration_identity(self.config.name, self.config.version) +922 try: +923 records = await handler.poll(inputs, last_poll_ts, context) +924 finally: +925 context._set_integration_identity(*previous_identity) +926 # Validate each record +927 for record in records: +928 if "id" not in record: +929 raise ValidationError( +930 f"Polling trigger '{name}' returned record without required 'id' field") +931 if "data" not in record: +932 raise ValidationError( +933 f"Polling trigger '{name}' returned record without required 'data' field") +934 +935 # Validate record data against output schema +936 try: +937 validate(record["data"], trigger_config.output_schema) +938 except Exception as e: +939 raise ValidationError(e.message, e.schema, e.instance) +940 +941 return recordsInherited Members
- + async def get_connected_account( self, context: ExecutionContext) -> IntegrationResult: @@ -4525,45 +4685,50 @@-Inherited Members
888 async def get_connected_account(self, context: ExecutionContext) -> IntegrationResult: -889 """Get connected account information -890 -891 Args: -892 context: Execution context -893 -894 Returns: -895 IntegrationResult containing connected account data -896 -897 Raises: -898 ValidationError: If no connected account handler is registered or auth is invalid -899 """ -900 if not self._connected_account_handler: -901 raise ValidationError("No connected account handler registered") -902 -903 if "fields" in self.config.auth: -904 auth_config = self.config.auth["fields"] -905 validator = Draft7Validator(auth_config) -906 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) -907 if errors: -908 message = "" -909 for error in errors: -910 message += f"{list(error.schema_path)}, {error.message},\n " -911 raise ValidationError(message, auth_config, context.auth) -912 -913 handler = self._connected_account_handler() -914 account_info = await handler.get_account_info(context) -915 -916 if not isinstance(account_info, ConnectedAccountInfo): -917 raise ValidationError( -918 f"Connected account handler must return ConnectedAccountInfo, got {type(account_info).__name__}" -919 ) -920 -921 # Return IntegrationResult with ConnectedAccountInfo object directly -922 return IntegrationResult( -923 version=__version__, -924 type=ResultType.CONNECTED_ACCOUNT, -925 result=account_info -926 ) +@@ -4765,4 +4930,4 @@943 async def get_connected_account(self, context: ExecutionContext) -> IntegrationResult: +944 """Get connected account information +945 +946 Args: +947 context: Execution context +948 +949 Returns: +950 IntegrationResult containing connected account data +951 +952 Raises: +953 ValidationError: If no connected account handler is registered or auth is invalid +954 """ +955 if not self._connected_account_handler: +956 raise ValidationError("No connected account handler registered") +957 +958 if "fields" in self.config.auth: +959 auth_config = self.config.auth["fields"] +960 validator = Draft7Validator(auth_config) +961 errors = sorted(validator.iter_errors(context.auth), key=lambda e: e.path) +962 if errors: +963 message = "" +964 for error in errors: +965 message += f"{list(error.schema_path)}, {error.message},\n " +966 raise ValidationError(message, auth_config, context.auth) +967 +968 handler = self._connected_account_handler() +969 previous_identity = (context._integration_name, context._integration_version) +970 context._set_integration_identity(self.config.name, self.config.version) +971 try: +972 account_info = await handler.get_account_info(context) +973 finally: +974 context._set_integration_identity(*previous_identity) +975 +976 if not isinstance(account_info, ConnectedAccountInfo): +977 raise ValidationError( +978 f"Connected account handler must return ConnectedAccountInfo, got {type(account_info).__name__}" +979 ) +980 +981 # Return IntegrationResult with ConnectedAccountInfo object directly +982 return IntegrationResult( +983 version=__version__, +984 type=ResultType.CONNECTED_ACCOUNT, +985 result=account_info +986 )Inherited Members
} });