1717from html import unescape
1818import json
1919import os
20+ import re
2021import shutil
22+ import subprocess
2123import tempfile
2224import time
2325from typing import (
5759}
5860
5961
62+ def _sanitize_api_name (name : str ) -> str :
63+ """Sanitize an API name to comply with Salesforce naming rules.
64+
65+ Replaces spaces and hyphens with underscores, removes invalid characters,
66+ collapses consecutive underscores, and strips leading/trailing underscores.
67+ """
68+ sanitized = re .sub (r"[ \-]" , "_" , name )
69+ sanitized = re .sub (r"[^\w]" , "" , sanitized )
70+ sanitized = re .sub (r"_+" , "_" , sanitized )
71+ sanitized = sanitized .strip ("_" )
72+ return sanitized
73+
74+
6075class CodeExtensionMetadata (BaseModel ):
6176 name : str
6277 version : str
@@ -66,6 +81,24 @@ class CodeExtensionMetadata(BaseModel):
6681 functionInvokeOptions : Union [list [str ], None ] = None
6782
6883 def __init__ (self , ** data ):
84+ name = data .get ("name" , "" )
85+ sanitized = _sanitize_api_name (name )
86+ if sanitized != name :
87+ logger .warning (f"API name '{ name } ' was sanitized to '{ sanitized } '" )
88+ data ["name" ] = sanitized
89+ if not sanitized :
90+ raise ValueError (
91+ f"API name '{ name } ' is invalid and could not be sanitized to a"
92+ " valid name."
93+ )
94+ if not sanitized [0 ].isalpha ():
95+ raise ValueError (
96+ f"API name '{ sanitized } ' must begin with a letter. "
97+ "The name can only contain underscores and alphanumeric"
98+ " characters, must begin with a letter, not include spaces,"
99+ " not end with an underscore, and not contain two consecutive"
100+ " underscores."
101+ )
69102 super ().__init__ (** data )
70103
71104
@@ -156,6 +189,54 @@ def _retrieve_access_token(credentials: Credentials) -> AccessTokenResponse:
156189 return AccessTokenResponse (** response )
157190
158191
192+ def _retrieve_access_token_from_sf_cli (sf_cli_org : str ) -> AccessTokenResponse :
193+ """Get an access token from the Salesforce CLI."""
194+ try :
195+ result = subprocess .run (
196+ ["sf" , "org" , "display" , "--target-org" , sf_cli_org , "--json" ],
197+ capture_output = True ,
198+ text = True ,
199+ check = True ,
200+ timeout = 30 ,
201+ )
202+ except FileNotFoundError as exc :
203+ raise RuntimeError (
204+ "The 'sf' command was not found. "
205+ "Please install Salesforce CLI: https://developer.salesforce.com/tools/salesforcecli"
206+ ) from exc
207+ except subprocess .TimeoutExpired as exc :
208+ raise RuntimeError (
209+ f"'sf org display' timed out for org '{ sf_cli_org } '"
210+ ) from exc
211+ except subprocess .CalledProcessError as exc :
212+ raise RuntimeError (
213+ f"'sf org display' failed for org '{ sf_cli_org } '.\n "
214+ f"Ensure the org is authenticated via 'sf org login web'.\n "
215+ f"stderr: { exc .stderr .strip ()} "
216+ ) from exc
217+
218+ try :
219+ data = json .loads (result .stdout )
220+ except json .JSONDecodeError as exc :
221+ raise RuntimeError (f"Failed to parse 'sf org display' output: { exc } " ) from exc
222+
223+ if data .get ("status" ) != 0 :
224+ raise RuntimeError (
225+ f"SF CLI error for org '{ sf_cli_org } ': "
226+ f"{ data .get ('message' , 'unknown error' )} "
227+ )
228+
229+ org_result = data .get ("result" , {})
230+ access_token = org_result .get ("accessToken" )
231+ instance_url = org_result .get ("instanceUrl" )
232+ if not access_token or not instance_url :
233+ raise RuntimeError (
234+ f"'sf org display' did not return an access token or instance URL "
235+ f"for org '{ sf_cli_org } '"
236+ )
237+ return AccessTokenResponse (access_token = access_token , instance_url = instance_url )
238+
239+
159240class CreateDeploymentResponse (BaseModel ):
160241 fileUploadUrl : str
161242
@@ -463,12 +544,15 @@ def zip(
463544def deploy_full (
464545 directory : str ,
465546 metadata : CodeExtensionMetadata ,
466- credentials : Credentials ,
547+ credentials : Union [ " Credentials" , AccessTokenResponse ] ,
467548 docker_network : str ,
468549 callback = None ,
469550) -> AccessTokenResponse :
470551 """Deploy a data transform in the DataCloud."""
471- access_token = _retrieve_access_token (credentials )
552+ if isinstance (credentials , AccessTokenResponse ):
553+ access_token = credentials
554+ else :
555+ access_token = _retrieve_access_token (credentials )
472556
473557 # prepare payload
474558 config = get_config (directory )
0 commit comments