Skip to content

Commit d47ad10

Browse files
Improving external SDK for function
1 parent 06425e4 commit d47ad10

9 files changed

Lines changed: 532 additions & 30 deletions

File tree

src/datacustomcode/cli.py

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ def _configure_client_credentials(
7474
)
7575

7676

77+
def _generate_function_test_file(entrypoint_path: str) -> Optional[str]:
78+
"""Generate test.json file for a function.
79+
80+
Args:
81+
entrypoint_path: Path to the function's entrypoint.py
82+
83+
Returns:
84+
Path to generated test.json, or None if generation failed
85+
"""
86+
from datacustomcode.template import generate_test_json
87+
88+
tests_dir = os.path.join(os.path.dirname(entrypoint_path), "tests")
89+
os.makedirs(tests_dir, exist_ok=True)
90+
test_json_path = os.path.join(tests_dir, "test.json")
91+
92+
try:
93+
generate_test_json(entrypoint_path, test_json_path)
94+
return test_json_path
95+
except Exception as e:
96+
logger.warning(f"Could not generate test.json: {e}")
97+
return None
98+
99+
77100
@cli.command()
78101
@click.option("--profile", default="default", help="Credential profile name")
79102
@click.option(
@@ -162,7 +185,6 @@ def zip(path: str, network: str):
162185
163186
Choose based on your workload requirements.""",
164187
)
165-
@click.option("--function-invoke-opt")
166188
@click.option(
167189
"--sf-cli-org",
168190
default=None,
@@ -176,13 +198,14 @@ def deploy(
176198
cpu_size: str,
177199
profile: str,
178200
network: str,
179-
function_invoke_opt: str,
180201
sf_cli_org: Optional[str],
181202
):
182203
from datacustomcode.deploy import (
183204
COMPUTE_TYPES,
184205
CodeExtensionMetadata,
206+
USE_IN_FEATURE_MAPPING_FOR_CONNECT_API,
185207
deploy_full,
208+
infer_use_in_feature,
186209
)
187210
from datacustomcode.token_provider import (
188211
CredentialsTokenProvider,
@@ -211,15 +234,21 @@ def deploy(
211234
)
212235

213236
if package_type == "function":
214-
if not function_invoke_opt:
237+
# Infer use_in_feature from function signature
238+
entrypoint_path = os.path.join(path, "entrypoint.py")
239+
use_in_feature = infer_use_in_feature(entrypoint_path)
240+
if use_in_feature:
241+
logger.info(f"Inferred use_in_feature: {use_in_feature}")
242+
else:
215243
click.secho(
216-
"Error: Function invoke options are required for function package type",
244+
"Error: Could not infer function invoke options. Please provide --use-in-feature",
217245
fg="red",
218246
)
219247
raise click.Abort()
220-
else:
221-
function_invoke_options = function_invoke_opt.split(",")
222-
metadata.functionInvokeOptions = function_invoke_options
248+
249+
# Map user-provided feature names to API names
250+
mapped_feature = USE_IN_FEATURE_MAPPING_FOR_CONNECT_API.get(use_in_feature, use_in_feature)
251+
metadata.functionInvokeOptions = [mapped_feature]
223252

224253
try:
225254
if sf_cli_org:
@@ -238,19 +267,29 @@ def deploy(
238267
@click.option(
239268
"--code-type", default="script", type=click.Choice(["script", "function"])
240269
)
241-
def init(directory: str, code_type: str):
270+
@click.option(
271+
"--use-in-feature",
272+
default="SearchIndexChunking",
273+
help="Feature to invoke the function (only applicable for functions). If not provided, will be inferred from function signature.",
274+
)
275+
def init(directory: str, code_type: str, use_in_feature: Optional[str]):
242276
from datacustomcode.scan import (
243277
dc_config_json_from_file,
244278
update_config,
245279
write_sdk_config,
246280
)
247-
from datacustomcode.template import copy_function_template, copy_script_template
281+
from datacustomcode.template import (
282+
copy_function_template,
283+
copy_script_template,
284+
)
248285

249286
click.echo("Copying template to " + click.style(directory, fg="blue", bold=True))
250287
if code_type == "script":
251288
copy_script_template(directory)
252289
elif code_type == "function":
253-
copy_function_template(directory)
290+
# Default to SearchIndexChunking if not provided
291+
feature = use_in_feature
292+
copy_function_template(directory, feature)
254293
entrypoint_path = os.path.join(directory, "payload", "entrypoint.py")
255294
config_location = os.path.join(os.path.dirname(entrypoint_path), "config.json")
256295

@@ -265,6 +304,7 @@ def init(directory: str, code_type: str):
265304
updated_config_json = update_config(entrypoint_path)
266305
with open(config_location, "w") as f:
267306
json.dump(updated_config_json, f, indent=2)
307+
268308
click.echo(
269309
"Start developing by updating the code in "
270310
+ click.style(entrypoint_path, fg="blue", bold=True)
@@ -275,6 +315,23 @@ def init(directory: str, code_type: str):
275315
+ " to automatically update config.json when you make changes to your code"
276316
)
277317

318+
# Generate test.json for functions
319+
if code_type == "function":
320+
test_json_path = _generate_function_test_file(entrypoint_path)
321+
if test_json_path:
322+
click.echo(
323+
"Generated test file at "
324+
+ click.style(test_json_path, fg="blue", bold=True)
325+
)
326+
click.echo(
327+
"Test your function locally with "
328+
+ click.style(
329+
f"datacustomcode run {entrypoint_path} --test_with {test_json_path}",
330+
fg="blue",
331+
bold=True,
332+
)
333+
)
334+
278335

279336
@cli.command()
280337
@click.argument("filename")
@@ -312,6 +369,12 @@ def scan(filename: str, config: str, dry_run: bool, no_requirements: bool):
312369
@click.option("--config-file", default=None)
313370
@click.option("--dependencies", default=[], multiple=True)
314371
@click.option("--profile", default="default")
372+
@click.option(
373+
"--test_with",
374+
default=None,
375+
type=click.Path(exists=True),
376+
help="Path to test JSON file for function testing",
377+
)
315378
@click.option(
316379
"--sf-cli-org",
317380
default=None,
@@ -322,10 +385,11 @@ def run(
322385
config_file: Union[str, None],
323386
dependencies: List[str],
324387
profile: str,
388+
test_with: Optional[str],
325389
sf_cli_org: Optional[str],
326390
):
327391
from datacustomcode.run import run_entrypoint
328392

329393
run_entrypoint(
330-
entrypoint, config_file, dependencies, profile, sf_cli_org=sf_cli_org
394+
entrypoint, config_file, dependencies, profile, test_file=test_with, sf_cli_org=sf_cli_org
331395
)

src/datacustomcode/deploy.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,47 @@ def _sanitize_api_name(name: str) -> str:
6565
return sanitized
6666

6767

68+
# Mapping from user-facing feature names to internal API names
69+
USE_IN_FEATURE_MAPPING_FOR_CONNECT_API = {
70+
"SearchIndexChunking": "UnstructuredChunking",
71+
}
72+
73+
# Mapping from Pydantic request/response types to feature names
74+
REQUEST_TYPE_TO_FEATURE = {
75+
"SearchIndexChunkingV1Request": "SearchIndexChunking",
76+
"SearchIndexChunkingV1Response": "SearchIndexChunking",
77+
}
78+
79+
def infer_use_in_feature(entrypoint_path: str) -> Union[str, None]:
80+
"""Infer the use_in_feature from function signature.
81+
82+
Checks both the request parameter type and return type annotation.
83+
Both must map to the same feature for a valid inference.
84+
85+
Args:
86+
entrypoint_path: Path to the entrypoint.py file
87+
88+
Returns:
89+
The feature name if both request and response match, None otherwise
90+
"""
91+
from datacustomcode.function_utils import inspect_function_types
92+
93+
request_type_name, response_type_name = inspect_function_types(entrypoint_path)
94+
95+
if not request_type_name or not response_type_name:
96+
return None
97+
98+
# Look up features for both types
99+
request_feature = REQUEST_TYPE_TO_FEATURE.get(request_type_name)
100+
response_feature = REQUEST_TYPE_TO_FEATURE.get(response_type_name)
101+
102+
# Both must be present and must match
103+
if request_feature and response_feature and request_feature == response_feature:
104+
return request_feature
105+
106+
return None
107+
108+
68109
class CodeExtensionMetadata(BaseModel):
69110
name: str
70111
version: str

src/datacustomcode/function/feature_types/chunking.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from pydantic import BaseModel, Field
2929

3030

31-
class DocElement(BaseModel):
31+
class SearchIndexDocElement(BaseModel):
3232
"""Document element to be chunked"""
3333

3434
text: str = Field(..., description="Text content to be chunked")
@@ -37,7 +37,7 @@ class DocElement(BaseModel):
3737
)
3838

3939

40-
class ChunkOutput(BaseModel):
40+
class SearchIndexChunkOutput(BaseModel):
4141
"""Output chunk from the chunking process"""
4242

4343
chunk_id: str = Field(..., description="UUID for this chunk")
@@ -55,20 +55,17 @@ class ChunkOutput(BaseModel):
5555
)
5656

5757

58-
class StatusResponse(BaseModel):
58+
class SearchIndexStatusResponse(BaseModel):
5959
"""Status response for operation"""
6060

6161
status_type: str = Field(..., description="'success' or 'error'")
6262
status_message: str = Field(..., description="Human-readable status")
6363

6464

65-
class UdsChunkingV1BatchRequest(BaseModel):
65+
class SearchIndexChunkingV1Request(BaseModel):
6666
"""Batch request for UDS chunking"""
6767

68-
version: Literal["v1"] = Field(
69-
default="v1", description="API version, must be 'v1'"
70-
)
71-
input: List[DocElement] = Field(
68+
input: List[SearchIndexDocElement] = Field(
7269
..., min_length=1, description="List of documents (min 1)"
7370
)
7471
max_characters: int = Field(..., description="Max chars per chunk (default: 100)")
@@ -77,13 +74,9 @@ class UdsChunkingV1BatchRequest(BaseModel):
7774
)
7875

7976

80-
class UdsChunkingV1BatchResponse(BaseModel):
77+
class SearchIndexChunkingV1Response(BaseModel):
8178
"""Batch response for UDS chunking"""
82-
83-
version: Literal["v1"] = Field(
84-
default="v1", description="API version, must be 'v1'"
85-
)
86-
output: List[ChunkOutput] = Field(
79+
output: List[SearchIndexChunkOutput] = Field(
8780
default_factory=list, description="Flat list of chunks from all docs"
8881
)
89-
status: StatusResponse = Field(..., description="Overall operation status")
82+
status: SearchIndexStatusResponse = Field(..., description="Overall operation status")

0 commit comments

Comments
 (0)