Skip to content

Commit 4896a84

Browse files
Support XML/SOAP proxy requests and expose connected account details (#148)
* Add raw_body support to ActionClient.request() for non-JSON payloads Adds a raw_body parameter (Union[bytes, str]) that bypasses JSON serialization, enabling XML/SOAP and other non-JSON request bodies to be proxied as-is. Includes a docstring update and a Salesforce SOAP integration test. * make generate * Support XML/SOAP proxy requests and expose connected account details without credentials - ActionClient.request() now accepts a raw_body (str or bytes) parameter for sending non-JSON payloads (e.g. XML/SOAP). Previously only JSON and form-encoded bodies were supported. - Added get_connected_account_details() method that returns account metadata (status, connector, api_config, etc.) without auth credentials. Useful for customers who need to inspect the api_config version before constructing a SOAP request, without exposing access/refresh tokens. - Added integration tests covering Salesforce SOAP deploy + checkDeployStatus flow, the REST SObjects list endpoint, and get_connected_account_details. * bumping up version * Address CodeRabbit review comments - Fix 401 retry dropping raw_body: retry now uses the same json/data logic as the original request instead of always falling back to body/form_data - Normalize selectors in get_connected_account_details: when connected_account_id is supplied, connection_name and identifier are cleared before the call; raises ValueError if neither selector set is provided - Strip authorization_details in GetConnectedAccountDetailsResponse.from_proto as defense-in-depth, regardless of what the server returns * Revert selector normalization and credential stripping — server handles these
1 parent 88c7982 commit 4896a84

12 files changed

Lines changed: 274 additions & 52 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ VENV_PYTHON := $(VENV_DIR)/bin/python
1212
VENV_PIP := $(VENV_PYTHON) -m pip
1313

1414
PROTO_REPO_URL := https://github.com/scalekit-inc/scalekit.git
15-
PROTO_REF ?= v0.1.114.0
15+
PROTO_REF ?= v0.1.116.0
1616
PROTO_SUBDIR := proto
1717

1818
TEMP_DIR := temp_scalekit

scalekit/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Single source of truth for the SDK version.
22
# Import this in setup.py and scalekit/core.py — never hardcode the version elsewhere.
3-
__version__ = "2.8.0"
3+
__version__ = "2.9.0"

scalekit/actions/actions.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional, Any, List, Dict, Union
22
import requests
3-
from scalekit.actions.types import ToolRequest,ExecuteToolResponse,MagicLinkResponse,ListConnectedAccountsResponse,DeleteConnectedAccountResponse,GetConnectedAccountAuthResponse,ToolInput, \
3+
from scalekit.actions.types import ToolRequest,ExecuteToolResponse,MagicLinkResponse,ListConnectedAccountsResponse,DeleteConnectedAccountResponse,GetConnectedAccountAuthResponse,GetConnectedAccountDetailsResponse,ToolInput, \
44
UpdateConnectedAccountResponse,CreateMcpConfigResponse,ListMcpConfigsResponse,UpdateMcpConfigResponse,DeleteMcpConfigResponse, \
55
EnsureMcpInstanceResponse,UpdateMcpInstanceResponse,GetMcpInstanceResponse,ListMcpInstancesResponse,DeleteMcpInstanceResponse,GetMcpInstanceAuthStateResponse, \
66
McpConfig,McpConfigConnectionToolMapping,VerifyConnectedAccountUserResponse
@@ -323,7 +323,51 @@ def get_connected_account(
323323

324324
# Convert proto to our GetConnectedAccountAuthResponse class
325325
return GetConnectedAccountAuthResponse.from_proto(proto_response)
326-
326+
327+
def get_connected_account_details(
328+
self,
329+
connection_name: Optional[str] = None,
330+
identifier: Optional[str] = None,
331+
connected_account_id: Optional[str] = None,
332+
**kwargs
333+
) -> GetConnectedAccountDetailsResponse:
334+
"""
335+
Get connected account details by identifier, without auth credentials.
336+
337+
Use this instead of :meth:`get_connected_account` when you only need
338+
account metadata (status, connector, api_config, etc.) and do not
339+
require the access/refresh tokens.
340+
341+
You must provide **one** of the following to identify the connected account:
342+
343+
- ``connection_name`` **and** ``identifier`` — use when you know the
344+
connector name and the end-user's identifier (e.g. email address).
345+
- ``connected_account_id`` — use when you already hold the Scalekit
346+
connected account ID.
347+
348+
:param connection_name: Connector identifier, e.g. ``"salesforce-1hpnGzcD"``.
349+
Required when ``connected_account_id`` is not provided.
350+
:type connection_name: str
351+
:param identifier: End-user identifier tied to the connected account,
352+
e.g. ``"john.doe"``. Required when ``connected_account_id`` is not provided.
353+
:type identifier: str
354+
:param connected_account_id: Scalekit connected account ID. When supplied,
355+
``connection_name`` and ``identifier`` are ignored.
356+
:type connected_account_id: str
357+
358+
:returns:
359+
GetConnectedAccountDetailsResponse containing account metadata
360+
without auth credentials
361+
:rtype: GetConnectedAccountDetailsResponse
362+
"""
363+
result_tuple = self.connected_accounts.get_connected_account_details_by_identifier(
364+
connector=connection_name,
365+
identifier=identifier,
366+
connected_account_id=connected_account_id
367+
)
368+
proto_response = result_tuple[0]
369+
return GetConnectedAccountDetailsResponse.from_proto(proto_response)
370+
327371
def add_modifier(self, modifier: Modifier) -> None:
328372
"""Add a modifier to the private list"""
329373
self._modifiers.append(modifier)
@@ -383,6 +427,7 @@ def request(
383427
query_params: Optional[Dict[str, Any]] = None,
384428
body: Optional[Any] = None,
385429
form_data: Optional[Dict[str, Any]] = None,
430+
raw_body: Optional[Union[bytes, str]] = None,
386431
headers: Optional[Dict[str, str]] = None,
387432
**kwargs,
388433
) -> requests.Response:
@@ -403,6 +448,14 @@ def request(
403448
:type body: Optional[Any]
404449
:param form_data: Request body sent as URL-encoded form data
405450
:type form_data: Optional[Dict[str, Any]]
451+
:param raw_body: Raw request body sent as-is, without any serialization.
452+
Use this **only** when the request payload is not JSON — for example,
453+
when the downstream API expects an XML body, a plain-text payload, or
454+
any other non-JSON content type. For JSON payloads use ``body`` instead.
455+
When ``raw_body`` is provided it takes priority over both ``body`` and
456+
``form_data``. You must also pass a matching ``Content-Type`` header via
457+
``headers`` (e.g. ``"Content-Type": "application/xml"``).
458+
:type raw_body: Optional[Union[bytes, str]]
406459
:param headers: Additional HTTP headers to merge into the request
407460
:type headers: Optional[Dict[str, str]]
408461
@@ -439,8 +492,8 @@ def request(
439492
method=method.upper(),
440493
url=url,
441494
params=params,
442-
json=body,
443-
data=form_data,
495+
json=body if raw_body is None else None,
496+
data=raw_body or form_data,
444497
headers=req_headers,
445498
timeout=timeout,
446499
**kwargs,
@@ -454,8 +507,8 @@ def request(
454507
method=method.upper(),
455508
url=url,
456509
params=params,
457-
json=body,
458-
data=form_data,
510+
json=body if raw_body is None else None,
511+
data=raw_body if raw_body is not None else form_data,
459512
headers=req_headers,
460513
timeout=timeout,
461514
**kwargs,

scalekit/actions/models/responses/get_connected_account_auth_response.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,41 @@ def to_dict(self) -> dict:
119119

120120
class Config:
121121
"""Pydantic configuration"""
122+
validate_assignment = True
123+
json_encoders = {
124+
datetime: lambda v: v.isoformat() if v else None
125+
}
126+
127+
128+
class GetConnectedAccountDetailsResponse(BaseModel):
129+
"""Connected account details response — same structure as GetConnectedAccountAuthResponse
130+
but auth credentials (access/refresh tokens) are omitted by the server."""
131+
132+
connected_account: Optional[ConnectedAccount] = Field(
133+
None,
134+
description="Connected account details (without auth credentials)"
135+
)
136+
137+
@classmethod
138+
def from_proto(cls, proto_response) -> 'GetConnectedAccountDetailsResponse':
139+
"""
140+
Create GetConnectedAccountDetailsResponse from protobuf GetConnectedAccountByIdentifierResponse
141+
142+
:param proto_response: The protobuf GetConnectedAccountByIdentifierResponse object
143+
:returns:
144+
GetConnectedAccountDetailsResponse instance
145+
"""
146+
connected_account = None
147+
if proto_response.connected_account:
148+
connected_account = ConnectedAccount.from_proto(proto_response.connected_account)
149+
return cls(connected_account=connected_account)
150+
151+
def to_dict(self) -> dict:
152+
return {
153+
"connected_account": self.connected_account.model_dump() if self.connected_account else None
154+
}
155+
156+
class Config:
122157
validate_assignment = True
123158
json_encoders = {
124159
datetime: lambda v: v.isoformat() if v else None

scalekit/actions/types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .models.responses.magic_link_response import MagicLinkResponse
77
from .models.responses.list_connected_accounts_response import ListConnectedAccountsResponse
88
from .models.responses.delete_connected_account_response import DeleteConnectedAccountResponse
9-
from .models.responses.get_connected_account_auth_response import GetConnectedAccountAuthResponse
9+
from .models.responses.get_connected_account_auth_response import GetConnectedAccountAuthResponse, GetConnectedAccountDetailsResponse
1010
from .models.responses.create_connected_account_response import CreateConnectedAccountResponse
1111
from .models.responses.update_connected_account_response import UpdateConnectedAccountResponse
1212
from .models.responses.create_mcp_response import CreateMcpResponse
@@ -38,6 +38,7 @@
3838
'ListConnectedAccountsResponse',
3939
'DeleteConnectedAccountResponse',
4040
'GetConnectedAccountAuthResponse',
41+
'GetConnectedAccountDetailsResponse',
4142
'CreateConnectedAccountResponse',
4243
'UpdateConnectedAccountResponse',
4344
'CreateMcpResponse',

scalekit/connected_accounts.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,42 @@ def get_connected_account_by_identifier(
275275
),
276276
)
277277

278+
def get_connected_account_details_by_identifier(
279+
self,
280+
connector: str,
281+
identifier: str,
282+
organization_id: Optional[str] = None,
283+
user_id: Optional[str] = None,
284+
connected_account_id: Optional[str] = None
285+
) -> GetConnectedAccountByIdentifierResponse:
286+
"""
287+
Method to get connected account details (without auth credentials) by identifier
288+
289+
:param connector : Connector identifier
290+
:type : ``` str ```
291+
:param identifier : Identifier for the connector
292+
:type : ``` str ```
293+
:param organization_id : Organization ID
294+
:type : ``` str ```
295+
:param user_id : User ID
296+
:type : ``` str ```
297+
:param connected_account_id : ID of the connected account
298+
:type : ``` str ```
299+
300+
:returns:
301+
Get Connected Account By Identifier Response (without auth credentials)
302+
"""
303+
return self.core_client.grpc_exec(
304+
self.connected_accounts_service.GetConnectedAccountDetails.with_call,
305+
GetConnectedAccountByIdentifierRequest(
306+
organization_id=organization_id,
307+
user_id=user_id,
308+
connector=connector,
309+
identifier=identifier,
310+
id=connected_account_id
311+
),
312+
)
313+
278314
def verify_connected_account_user(
279315
self,
280316
auth_request_id: str,

scalekit/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class CoreClient:
2929

3030
sdk_version = f"Scalekit-Python/{_sdk_version}"
3131
# YYYYMMDD
32-
api_version = "20260421"
32+
api_version = "20260428"
3333
user_agent = f"{sdk_version} Python/{platform.python_version()} ({platform.system()}; {platform.architecture()}"
3434

3535
def __init__(self, env_url, client_id, client_secret):

0 commit comments

Comments
 (0)