salesforce-data-customcode 6.0.3.dev2__tar.gz → 6.0.4.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/PKG-INFO +1 -1
  2. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/pyproject.toml +2 -1
  3. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/cli.py +6 -3
  4. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/deploy.py +109 -19
  5. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/writer/base.py +11 -1
  6. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/run.py +15 -3
  7. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +1 -1
  8. salesforce_data_customcode-6.0.4.dev1/src/datacustomcode/templates/function/payload/entrypoint.py +67 -0
  9. salesforce_data_customcode-6.0.4.dev1/src/datacustomcode/templates/function/payload/utility.py +104 -0
  10. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +1 -1
  11. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/token_provider.py +70 -36
  12. salesforce_data_customcode-6.0.3.dev2/src/datacustomcode/templates/function/payload/entrypoint.py +0 -173
  13. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/LICENSE.txt +0 -0
  14. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/README.md +0 -0
  15. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/__init__.py +0 -0
  16. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/auth.py +0 -0
  17. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/client.py +0 -0
  18. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/cmd.py +0 -0
  19. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/common_config.py +0 -0
  20. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/config.py +0 -0
  21. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/config.yaml +0 -0
  22. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/constants.py +0 -0
  23. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/credentials.py +0 -0
  24. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_platform_client.py +0 -0
  25. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_platform_config.py +0 -0
  26. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
  27. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_predictions/base.py +0 -0
  28. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
  29. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_predictions/types.py +0 -0
  30. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/einstein_predictions_config.py +0 -0
  31. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/file/__init__.py +0 -0
  32. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/file/base.py +0 -0
  33. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/file/path/__init__.py +0 -0
  34. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/file/path/default.py +0 -0
  35. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/function/__init__.py +0 -0
  36. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/function/base.py +0 -0
  37. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/function/feature_types/__init__.py +0 -0
  38. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/function/feature_types/chunking.py +0 -0
  39. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/function/runtime.py +0 -0
  40. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/function_utils.py +0 -0
  41. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/__init__.py +0 -0
  42. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/base.py +0 -0
  43. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/reader/__init__.py +0 -0
  44. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/reader/base.py +0 -0
  45. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/reader/query_api.py +0 -0
  46. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/reader/sf_cli.py +0 -0
  47. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/reader/utils.py +0 -0
  48. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/writer/__init__.py +0 -0
  49. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/writer/csv.py +0 -0
  50. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/io/writer/print.py +0 -0
  51. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/__init__.py +0 -0
  52. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/base.py +0 -0
  53. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/default.py +0 -0
  54. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/errors.py +0 -0
  55. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/spark_base.py +0 -0
  56. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/spark_default.py +0 -0
  57. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
  58. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
  59. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
  60. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
  61. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
  62. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/llm_gateway_config.py +0 -0
  63. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/mixin.py +0 -0
  64. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/py.typed +0 -0
  65. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/scan.py +0 -0
  66. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/spark/__init__.py +0 -0
  67. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/spark/base.py +0 -0
  68. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/spark/default.py +0 -0
  69. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/template.py +0 -0
  70. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/__init__.py +0 -0
  71. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  72. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/README.md +0 -0
  73. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  74. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
  75. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
  76. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
  77. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
  78. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
  79. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
  80. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
  81. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
  82. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
  83. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
  84. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/payload/config.json +0 -0
  85. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  86. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/function/requirements.txt +0 -0
  87. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/Dockerfile +0 -0
  88. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  89. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/README.md +0 -0
  90. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/account.ipynb +0 -0
  91. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  92. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
  93. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
  94. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  95. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/payload/config.json +0 -0
  96. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/payload/entrypoint.py +0 -0
  97. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  98. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/templates/script/requirements.txt +0 -0
  99. {salesforce_data_customcode-6.0.3.dev2 → salesforce_data_customcode-6.0.4.dev1}/src/datacustomcode/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: salesforce-data-customcode
3
- Version: 6.0.3.dev2
3
+ Version: 6.0.4.dev1
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -18,7 +18,7 @@ license = "Apache-2.0"
18
18
  name = "salesforce-data-customcode"
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.10,<3.12"
21
- version = "6.0.3.dev2"
21
+ version = "6.0.4.dev1"
22
22
 
23
23
  [tool.black]
24
24
  exclude = '''
@@ -114,6 +114,7 @@ build = "*"
114
114
  coverage = ">=7.0.0,<8.0.0"
115
115
  ipykernel = "^6.29.5"
116
116
  mypy = "*"
117
+ pipreqs = "*"
117
118
  poetry-dynamic-versioning = {extras = ["plugin"], version = "^1.8.2"}
118
119
  pre-commit = "*"
119
120
  pytest = "*"
@@ -198,6 +198,7 @@ def zip(path: str, network: str):
198
198
  default=None,
199
199
  help="SF CLI org alias or username. Fetches credentials via `sf org display`.",
200
200
  )
201
+ @click.option("--use-in-feature", default=None, hidden=True, deprecated=True)
201
202
  def deploy(
202
203
  path: str,
203
204
  name: str,
@@ -207,6 +208,7 @@ def deploy(
207
208
  profile: str,
208
209
  network: str,
209
210
  sf_cli_org: Optional[str],
211
+ use_in_feature: Optional[str],
210
212
  ):
211
213
  from datacustomcode.constants import USE_IN_FEATURE_MAPPING_FOR_CONNECT_API
212
214
  from datacustomcode.deploy import (
@@ -249,13 +251,14 @@ def deploy(
249
251
  logger.info(f"Inferred use_in_feature: {use_in_feature}")
250
252
  else:
251
253
  click.secho(
252
- "Error: Could not infer function invoke options. "
253
- "Please provide --use-in-feature",
254
+ "Error: Function signature does not match a supported type. "
255
+ "Use SearchIndexChunkingV1Request and "
256
+ "SearchIndexChunkingV1Response in function signature.",
254
257
  fg="red",
255
258
  )
256
259
  raise click.Abort()
257
260
 
258
- # Map user-provided feature names to API names
261
+ # Map feature names to Connect API names
259
262
  mapped_feature = USE_IN_FEATURE_MAPPING_FOR_CONNECT_API.get(
260
263
  use_in_feature, use_in_feature
261
264
  )
@@ -26,12 +26,13 @@ from typing import (
26
26
  Callable,
27
27
  Dict,
28
28
  List,
29
+ Optional,
29
30
  Union,
30
31
  )
31
32
 
32
33
  from loguru import logger
33
34
  import pydantic
34
- from pydantic import BaseModel
35
+ from pydantic import BaseModel, model_validator
35
36
  import requests
36
37
 
37
38
  from datacustomcode.cmd import cmd_output
@@ -366,23 +367,86 @@ class BaseConfig(BaseModel):
366
367
  entryPoint: str
367
368
 
368
369
 
370
+ class DataObjectField(BaseModel):
371
+ name: str
372
+ label: str
373
+ dataType: str
374
+ isPrimaryKey: bool = False
375
+ keyQualifierFieldName: Optional[str] = None
376
+
377
+
378
+ class DataObject(BaseModel):
379
+ name: str
380
+ label: str
381
+ type: str
382
+ category: str
383
+ fields: list[DataObjectField]
384
+
385
+
369
386
  class DataTransformConfig(BaseConfig):
370
387
  sdkVersion: str
371
388
  dataspace: str
372
389
  permissions: Permissions
390
+ dataObjects: Optional[list[DataObject]] = None
373
391
 
374
392
 
375
393
  class FunctionConfig(BaseConfig):
376
394
  pass
377
395
 
378
396
 
397
+ class DloPermission(BaseModel):
398
+ dlo: list[str]
399
+
400
+
401
+ class DmoPermission(BaseModel):
402
+ dmo: list[str]
403
+
404
+
379
405
  class Permissions(BaseModel):
380
- read: Union[DloPermission]
381
- write: Union[DloPermission]
406
+ read: Union[DloPermission, DmoPermission]
407
+ write: Union[DloPermission, DmoPermission]
408
+
409
+ @model_validator(mode="after")
410
+ def _no_mixed_layers(self) -> "Permissions":
411
+ read_is_dlo = isinstance(self.read, DloPermission)
412
+ write_is_dlo = isinstance(self.write, DloPermission)
413
+ if read_is_dlo != write_is_dlo:
414
+ raise ValueError(
415
+ "permissions.read and permissions.write must both reference "
416
+ "DLOs or both reference DMOs (got "
417
+ f"read={type(self.read).__name__}, "
418
+ f"write={type(self.write).__name__})"
419
+ )
420
+ return self
382
421
 
383
422
 
384
- class DloPermission(BaseModel):
385
- dlo: list[str]
423
+ def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
424
+ """Return the list of object names regardless of layer (DLO or DMO)."""
425
+ if isinstance(perm, DloPermission):
426
+ return perm.dlo
427
+ return perm.dmo
428
+
429
+
430
+ def _data_object_to_output(obj: DataObject) -> dict[str, Any]:
431
+ """Convert a config.json DataObject into an outputDataObjects entry."""
432
+ fields: list[dict[str, Any]] = []
433
+ for field in obj.fields:
434
+ entry: dict[str, Any] = {
435
+ "isPrimaryKey": field.isPrimaryKey,
436
+ "label": field.label,
437
+ "name": field.name,
438
+ "type": field.dataType,
439
+ }
440
+ if field.keyQualifierFieldName is not None:
441
+ entry["keyQualifierField"] = field.keyQualifierFieldName
442
+ fields.append(entry)
443
+ return {
444
+ "category": obj.category,
445
+ "fields": fields,
446
+ "label": obj.label,
447
+ "name": obj.name,
448
+ "type": obj.type,
449
+ }
386
450
 
387
451
 
388
452
  def get_config(directory: str) -> BaseConfig:
@@ -404,10 +468,17 @@ def get_config(directory: str) -> BaseConfig:
404
468
  except json.JSONDecodeError as err:
405
469
  raise ValueError(f"config.json at {config_path} is not valid JSON") from err
406
470
  except pydantic.ValidationError as err:
407
- missing_fields = [str(err["loc"][0]) for err in err.errors()]
471
+ errors = err.errors()
472
+ missing = [e for e in errors if e.get("type") == "missing"]
473
+ if missing and len(missing) == len(errors):
474
+ missing_fields = [str(e["loc"][0]) for e in missing]
475
+ raise ValueError(
476
+ f"config.json at {config_path} is missing required "
477
+ f"fields: {', '.join(missing_fields)}"
478
+ ) from err
479
+ messages = [str(e.get("msg", "")) for e in errors]
408
480
  raise ValueError(
409
- f"config.json at {config_path} is missing required "
410
- f"fields: {', '.join(missing_fields)}"
481
+ f"config.json at {config_path} is invalid: {'; '.join(messages)}"
411
482
  ) from err
412
483
 
413
484
 
@@ -421,26 +492,45 @@ def create_data_transform(
421
492
  script_name = metadata.name
422
493
  request_hydrated = DATA_TRANSFORM_REQUEST_TEMPLATE.copy()
423
494
 
424
- # Add nodes for each write DLO
425
- for i, dlo in enumerate(data_transform_config.permissions.write.dlo, 1):
495
+ # Add nodes for each write entry (DLO or DMO)
496
+ for i, name in enumerate(
497
+ _permission_entries(data_transform_config.permissions.write), 1
498
+ ):
426
499
  request_hydrated["nodes"][f"node{i}"] = {
427
- "relation_name": dlo,
500
+ "relation_name": name,
428
501
  "config": {"materialized": "table"},
429
502
  "compiled_code": "",
430
503
  }
431
504
 
432
- # Add sources for each read DLO
433
- for i, dlo in enumerate(data_transform_config.permissions.read.dlo, 1):
434
- request_hydrated["sources"][f"source{i}"] = {"relation_name": dlo}
505
+ # Add sources for each read entry (DLO or DMO)
506
+ for i, name in enumerate(
507
+ _permission_entries(data_transform_config.permissions.read), 1
508
+ ):
509
+ request_hydrated["sources"][f"source{i}"] = {"relation_name": name}
435
510
 
436
511
  request_hydrated["macros"]["macro.byoc"]["arguments"][0]["name"] = script_name
437
512
 
513
+ definition: dict[str, Any] = {
514
+ "type": "DCSQL",
515
+ "manifest": request_hydrated,
516
+ "version": "56.0",
517
+ }
518
+
519
+ # outputDataObjects is only set for DMO-backed transforms. The server requires
520
+ # the schema of any DMO created/updated by the transform; DLO transforms use
521
+ # an existing materialized table and must not include this field.
522
+ if isinstance(data_transform_config.permissions.write, DmoPermission):
523
+ if not data_transform_config.dataObjects:
524
+ raise ValueError(
525
+ "DMO transforms require 'dataObjects' in config.json describing "
526
+ "the schema of each output DMO."
527
+ )
528
+ definition["outputDataObjects"] = [
529
+ _data_object_to_output(obj) for obj in data_transform_config.dataObjects
530
+ ]
531
+
438
532
  body = {
439
- "definition": {
440
- "type": "DCSQL",
441
- "manifest": request_hydrated,
442
- "version": "56.0",
443
- },
533
+ "definition": definition,
444
534
  "label": f"{metadata.name}",
445
535
  "name": f"{metadata.name}",
446
536
  "type": "BATCH",
@@ -27,11 +27,21 @@ if TYPE_CHECKING:
27
27
  class WriteMode(str, Enum):
28
28
  APPEND = "append"
29
29
  OVERWRITE = "overwrite"
30
- OVERWRITE_PARTITIONS = "overwrite_partitions"
30
+ OVERWRITE_PARTITIONS = "overwrite_partitions" # Deprecated: raises error if used
31
31
  MERGE = "merge"
32
32
  MERGE_UPSERT_DELETE = "merge_upsert_delete"
33
33
 
34
34
 
35
+ class MergeRecordType(str, Enum):
36
+ """Values for the _merge_record_type column used by MERGE_UPSERT_DELETE."""
37
+
38
+ UPSERT = "UPSERT"
39
+ DELETE = "DELETE"
40
+
41
+
42
+ MERGE_RECORD_TYPE_COLUMN = "_merge_record_type"
43
+
44
+
35
45
  class BaseDataCloudWriter(BaseDataAccessLayer):
36
46
  """Base class for Data Cloud writers."""
37
47
 
@@ -201,8 +201,20 @@ def run_function_with_test(entrypoint: str, test_file: str) -> None:
201
201
 
202
202
 
203
203
  def add_py_folder(entrypoint: str):
204
- default_py_folder = "py-files" # Hardcoded folder name
204
+ """Add py-files subfolder and entrypoint directory to sys.path.
205
+
206
+ This ensures:
207
+ 1. py-files/ is available for additional dependencies
208
+ 2. The entrypoint directory is available for local module imports
209
+ """
210
+ default_py_folder = "py-files"
205
211
  cwd = Path.cwd().joinpath(entrypoint)
206
- py_folder = cwd.parent.joinpath(default_py_folder)
212
+ entrypoint_dir = cwd.parent
213
+ py_folder = entrypoint_dir.joinpath(default_py_folder)
214
+
215
+ # Add py-files folder if it exists
216
+ if py_folder.exists():
217
+ sys.path.insert(0, str(py_folder))
207
218
 
208
- sys.path.append(str(py_folder))
219
+ # Add entrypoint directory to allow local module imports
220
+ sys.path.insert(0, str(entrypoint_dir))
@@ -5,6 +5,6 @@
5
5
  "dockerfile": "../Dockerfile"
6
6
  },
7
7
  "features": {
8
- "ghcr.io/devcontainers/features/git:1": {},
8
+ "ghcr.io/devcontainers/features/git:1": {}
9
9
  }
10
10
  }
@@ -0,0 +1,67 @@
1
+ import logging
2
+
3
+ from utility import extract_citations, split_text_into_chunks
4
+
5
+ from datacustomcode.function import Runtime
6
+ from datacustomcode.function.feature_types.chunking import (
7
+ ChunkType,
8
+ SearchIndexChunkingV1Output,
9
+ SearchIndexChunkingV1Request,
10
+ SearchIndexChunkingV1Response,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+ logging.basicConfig(level=logging.INFO)
15
+
16
+ # Default max chunk size (can be overridden if contract adds max_characters field)
17
+ DEFAULT_MAX_CHUNK_SIZE = 50
18
+
19
+
20
+ def function(
21
+ request: SearchIndexChunkingV1Request, runtime: Runtime
22
+ ) -> SearchIndexChunkingV1Response:
23
+ """Chunk documents into smaller pieces for search indexing.
24
+
25
+ Args:
26
+ request: SearchIndexChunkingV1Request with input documents
27
+ runtime: Runtime context (unused but required by contract)
28
+
29
+ Returns:
30
+ SearchIndexChunkingV1Response with chunked output
31
+ """
32
+ print(f"Received {len(request.input)} documents to chunk")
33
+
34
+ chunks = []
35
+ seq_no = 1
36
+
37
+ # Use default max chunk size
38
+ max_chunk_size = DEFAULT_MAX_CHUNK_SIZE
39
+
40
+ # Process each document
41
+ for doc_idx, doc in enumerate(request.input):
42
+ text = doc.text
43
+ metadata = doc.metadata
44
+
45
+ print(f"Processing document {doc_idx + 1}: {len(text)} characters")
46
+
47
+ # Split the text using our simple chunking algorithm
48
+ text_chunks = split_text_into_chunks(text, max_chunk_size, overlap=20)
49
+
50
+ # Create chunk outputs
51
+ for chunk_text in text_chunks:
52
+ citations = extract_citations(metadata)
53
+
54
+ chunk_output = SearchIndexChunkingV1Output(
55
+ chunk_type=ChunkType.TEXT,
56
+ text=chunk_text.strip(),
57
+ seq_no=seq_no,
58
+ citations=citations,
59
+ )
60
+ chunks.append(chunk_output)
61
+
62
+ print(f"Chunk {seq_no}: {len(chunk_text)} chars")
63
+ seq_no += 1
64
+
65
+ print(f"Generated {len(chunks)} chunks total")
66
+
67
+ return SearchIndexChunkingV1Response(output=chunks)
@@ -0,0 +1,104 @@
1
+ """Utility functions for text chunking operations."""
2
+
3
+ import logging
4
+ from typing import (
5
+ Dict,
6
+ List,
7
+ Optional,
8
+ )
9
+
10
+ from datacustomcode.function.feature_types.chunking import SearchIndexChunkingV1Metadata
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def split_text_into_chunks(text: str, max_size: int, overlap: int = 20) -> List[str]:
16
+ """Split text into chunks with overlap, trying to break at natural boundaries.
17
+
18
+ Tries to break at natural boundaries in order of preference:
19
+ 1. Paragraph boundaries (\\n\\n)
20
+ 2. Line boundaries (\\n)
21
+ 3. Sentence boundaries (. ! ?)
22
+ 4. Word boundaries (space)
23
+ 5. Hard cut if no good boundary found
24
+
25
+ Args:
26
+ text: Text to split
27
+ max_size: Maximum characters per chunk
28
+ overlap: Number of characters to overlap between chunks
29
+
30
+ Returns:
31
+ List of text chunks
32
+ """
33
+ if len(text) <= max_size:
34
+ return [text]
35
+
36
+ chunks = []
37
+ start = 0
38
+
39
+ while start < len(text):
40
+ # Determine end position for this chunk
41
+ end = start + max_size
42
+
43
+ if end >= len(text):
44
+ # Last chunk
45
+ chunks.append(text[start:])
46
+ break
47
+
48
+ # Try to find a good breaking point (in order of preference)
49
+ chunk_text = text[start:end]
50
+ break_point = None
51
+
52
+ # Try to break at paragraph boundary (\n\n)
53
+ last_paragraph = chunk_text.rfind("\n\n")
54
+ if last_paragraph > max_size * 0.5: # Only if it's past halfway
55
+ break_point = start + last_paragraph + 2 # +2 to skip the \n\n
56
+
57
+ # Try to break at line boundary (\n)
58
+ if break_point is None:
59
+ last_newline = chunk_text.rfind("\n")
60
+ if last_newline > max_size * 0.5:
61
+ break_point = start + last_newline + 1
62
+
63
+ # Try to break at sentence boundary (. ! ?)
64
+ if break_point is None:
65
+ for punct in [". ", "! ", "? "]:
66
+ last_sentence = chunk_text.rfind(punct)
67
+ if last_sentence > max_size * 0.5:
68
+ break_point = start + last_sentence + len(punct)
69
+ break
70
+
71
+ # Try to break at word boundary (space)
72
+ if break_point is None:
73
+ last_space = chunk_text.rfind(" ")
74
+ if last_space > max_size * 0.5:
75
+ break_point = start + last_space + 1
76
+
77
+ # If no good breaking point, just hard cut
78
+ if break_point is None:
79
+ break_point = end
80
+
81
+ chunks.append(text[start:break_point].strip())
82
+
83
+ # Move start position with overlap
84
+ start = max(break_point - overlap, start + 1)
85
+
86
+ return chunks
87
+
88
+
89
+ def extract_citations(
90
+ metadata: Optional[SearchIndexChunkingV1Metadata],
91
+ ) -> Dict[str, str]:
92
+ """Extract citations from document metadata.
93
+
94
+ Args:
95
+ metadata: Document metadata containing source DMO fields
96
+
97
+ Returns:
98
+ Dictionary of citation key-value pairs
99
+ """
100
+ citations = {}
101
+ if metadata and metadata.source_dmo_fields:
102
+ for key, value in metadata.source_dmo_fields.items():
103
+ citations[key] = str(value)
104
+ return citations
@@ -5,6 +5,6 @@
5
5
  "dockerfile": "../Dockerfile"
6
6
  },
7
7
  "features": {
8
- "ghcr.io/devcontainers/features/git:1": {},
8
+ "ghcr.io/devcontainers/features/git:1": {}
9
9
  }
10
10
  }
@@ -101,49 +101,83 @@ class SFCLITokenProvider(TokenProvider):
101
101
 
102
102
  from datacustomcode.deploy import AccessTokenResponse
103
103
 
104
- try:
105
- result = subprocess.run(
106
- ["sf", "org", "display", "--target-org", self.sf_cli_org, "--json"],
107
- capture_output=True,
108
- text=True,
109
- check=True,
110
- timeout=30,
111
- )
112
- except FileNotFoundError as exc:
113
- raise RuntimeError(
114
- "The 'sf' command was not found. "
115
- "Install Salesforce CLI: https://developer.salesforce.com/tools/salesforcecli"
116
- ) from exc
117
- except subprocess.TimeoutExpired as exc:
118
- raise RuntimeError(
119
- f"'sf org display' timed out for org '{self.sf_cli_org}'"
120
- ) from exc
121
- except subprocess.CalledProcessError as exc:
122
- raise RuntimeError(
123
- f"'sf org display' failed for org '{self.sf_cli_org}': {exc.stderr}"
124
- ) from exc
104
+ def _run_sf_command(args: list[str], description: str) -> dict:
105
+ try:
106
+ result = subprocess.run(
107
+ args,
108
+ capture_output=True,
109
+ text=True,
110
+ check=True,
111
+ timeout=30,
112
+ )
113
+ except FileNotFoundError as exc:
114
+ raise RuntimeError(
115
+ "The 'sf' command was not found. "
116
+ "Install Salesforce CLI: "
117
+ "https://developer.salesforce.com/tools/salesforcecli"
118
+ ) from exc
119
+ except subprocess.TimeoutExpired as exc:
120
+ raise RuntimeError(
121
+ f"'{description}' timed out for org '{self.sf_cli_org}'"
122
+ ) from exc
123
+ except subprocess.CalledProcessError as exc:
124
+ raise RuntimeError(
125
+ f"'{description}' failed for org '{self.sf_cli_org}': "
126
+ f"{exc.stderr}"
127
+ ) from exc
125
128
 
126
- try:
127
- data = json.loads(result.stdout)
128
- except json.JSONDecodeError as exc:
129
- raise RuntimeError(
130
- f"Failed to parse JSON from 'sf org display': {result.stdout}"
131
- ) from exc
129
+ try:
130
+ data = json.loads(result.stdout)
131
+ except json.JSONDecodeError as exc:
132
+ raise RuntimeError(
133
+ f"Failed to parse JSON from '{description}': {result.stdout}"
134
+ ) from exc
132
135
 
133
- if data.get("status") != 0:
136
+ if data.get("status") != 0:
137
+ raise RuntimeError(
138
+ f"SF CLI error for org '{self.sf_cli_org}': "
139
+ f"{data.get('message', 'unknown error')}"
140
+ )
141
+ return dict(data)
142
+
143
+ # Get org info from sf org display
144
+ display_data = _run_sf_command(
145
+ ["sf", "org", "display", "--target-org", self.sf_cli_org, "--json"],
146
+ "sf org display",
147
+ )
148
+ result_data = display_data.get("result", {})
149
+ instance_url = result_data.get("instanceUrl")
150
+ if not instance_url:
134
151
  raise RuntimeError(
135
- f"SF CLI error for org '{self.sf_cli_org}': "
136
- f"{data.get('message', 'unknown error')}"
152
+ f"'sf org display' did not return an instance URL "
153
+ f"for org '{self.sf_cli_org}'"
137
154
  )
138
155
 
139
- result_data = data.get("result", {})
140
- access_token = result_data.get("accessToken")
141
- instance_url = result_data.get("instanceUrl")
156
+ # Try show-access-token first (SF CLI >= 2.136.6); fall back to the
157
+ # token from sf org display (older CLIs don't redact it).
158
+ access_token = None
159
+ try:
160
+ token_data = _run_sf_command(
161
+ [
162
+ "sf",
163
+ "org",
164
+ "auth",
165
+ "show-access-token",
166
+ "--target-org",
167
+ self.sf_cli_org,
168
+ "--json",
169
+ ],
170
+ "sf org auth show-access-token",
171
+ )
172
+ access_token = token_data.get("result", {}).get("accessToken")
173
+ except RuntimeError:
174
+ # Command not available on older SF CLI versions
175
+ access_token = result_data.get("accessToken")
142
176
 
143
- if not access_token or not instance_url:
177
+ if not access_token:
144
178
  raise RuntimeError(
145
- f"'sf org display' did not return an access token or instance URL "
146
- f"for org '{self.sf_cli_org}'"
179
+ f"Could not obtain an access token for org '{self.sf_cli_org}'. "
180
+ f"Upgrade SF CLI to 2.136.6+ or ensure the org is authenticated."
147
181
  )
148
182
 
149
183
  return AccessTokenResponse(access_token=access_token, instance_url=instance_url)
@@ -1,173 +0,0 @@
1
- import logging
2
- from typing import List
3
- from uuid import uuid4
4
-
5
- from datacustomcode.einstein_predictions.types import (
6
- PredictionColumBuilder,
7
- PredictionRequestBuilder,
8
- PredictionType,
9
- )
10
- from datacustomcode.function import Runtime
11
- from datacustomcode.llm_gateway.types.generate_text_request_builder import (
12
- GenerateTextRequestBuilder,
13
- )
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- def chunk_text(text: str, chunk_size: int = 1000) -> List[str]:
19
- """
20
- Split text into chunks of approximately chunk_size characters.
21
- Tries to split at sentence boundaries when possible.
22
- """
23
- if not text:
24
- return []
25
-
26
- chunks = []
27
- current_chunk = ""
28
-
29
- # Split text into sentences (simple split by period)
30
- sentences = text.split(". ")
31
-
32
- for sentence in sentences:
33
- if len(current_chunk) + len(sentence) <= chunk_size:
34
- current_chunk += sentence + ". "
35
- else:
36
- if current_chunk:
37
- chunks.append(current_chunk.strip())
38
- current_chunk = sentence + ". "
39
-
40
- if current_chunk:
41
- chunks.append(current_chunk.strip())
42
-
43
- return chunks
44
-
45
-
46
- def make_einstein_prediction(runtime: Runtime) -> None:
47
- column = (
48
- PredictionColumBuilder()
49
- .set_column_name("col1")
50
- .set_string_values(["str1", "str2"])
51
- .build()
52
- )
53
- prediction_request = (
54
- PredictionRequestBuilder()
55
- .set_prediction_type(PredictionType.REGRESSION)
56
- .set_model_api_name("regressionModel")
57
- .set_prediction_columns([column])
58
- .build()
59
- )
60
-
61
- prediction_response = runtime.einstein_predictions.predict(prediction_request)
62
- logger.info(
63
- f"Einstein prediction results - success: [{prediction_response.is_success}] "
64
- f"response data: {prediction_response.data}"
65
- )
66
-
67
-
68
- def generate_text(runtime: Runtime, prompt: str, model: str = "sfdc_ai__DefaultGPT52"):
69
- builder = GenerateTextRequestBuilder()
70
- llm_request = builder.set_prompt(prompt).set_model(model).build()
71
- llm_response = runtime.llm_gateway.generate_text(llm_request)
72
- logger.info(
73
- f"LLM Gateway generate text results - success: [{llm_response.is_success}] "
74
- f"response data: {llm_response.data}"
75
- )
76
-
77
-
78
- def function(request: dict, runtime: Runtime) -> dict:
79
- logger.info("Inside Function")
80
- logger.info(request)
81
-
82
- items = request["input"]
83
- output_chunks = []
84
- current_seq_no = 1 # Start sequence number from 1
85
-
86
- """
87
- You can use your AI models configured in Salesforce to generate texts
88
- or predict an outcome. See README.md for how to test locally before
89
- deploying to Data Cloud.
90
-
91
- Example:
92
-
93
- >>> generated_text = generate_text(runtime, "Generate a greeting message")
94
- ... prediction = make_einstein_prediction(runtime)
95
-
96
- """
97
-
98
- for item in items:
99
- # Item is DocElement as dict
100
- logger.info(f"Processing item: {item}")
101
-
102
- text = item.get("text", "")
103
- metadata = item.get("metadata", {})
104
-
105
- # Create chunks from the text
106
- text_chunks = chunk_text(text, chunk_size=100) # Using a larger chunk size
107
-
108
- # Create chunk dictionaries for each text chunk
109
- for chunk_content in text_chunks:
110
- chunk_dict = {
111
- "text": chunk_content,
112
- "metadata": metadata,
113
- "seq_no": current_seq_no,
114
- "chunk_type": "text",
115
- "chunk_id": str(uuid4()),
116
- "tag_metadata": {},
117
- "citations": {},
118
- "source_record": item,
119
- }
120
- output_chunks.append(chunk_dict)
121
- current_seq_no += 1 # Increment sequence number for next chunk
122
-
123
- logger.info("Completed chunking")
124
- response = {
125
- "output": output_chunks,
126
- "status": {"status_type": "success", "status_message": "Chunking completed"},
127
- }
128
- logger.info(response)
129
- return response
130
-
131
-
132
- # Test the function
133
- if __name__ == "__main__":
134
- # Configure logging
135
- logging.basicConfig(level=logging.INFO)
136
-
137
- # Create test data with two DocElements
138
- test_request = {
139
- "input": [
140
- {
141
- "text": (
142
- """This is the first sentence of the first document, which is
143
- intentionally made longer to test chunking. """
144
- """Here is the second sentence of the first document, which is also
145
- quite long and should ensure that the chunking function splits
146
- this text into two chunks when the chunk size is set to 100."""
147
- ),
148
- "metadata": {"source": "test1", "type": "document"},
149
- },
150
- {
151
- "text": (
152
- """This is the first sentence of the second document, and it is
153
- also extended to be longer than usual for testing purposes. """
154
- """The second sentence of the second document is similarly lengthy,
155
- so that the chunking function will again create two chunks for
156
- this document."""
157
- ),
158
- "metadata": {"source": "test2", "type": "document"},
159
- },
160
- ]
161
- }
162
-
163
- # Run the function
164
- result = function(test_request, Runtime())
165
-
166
- # Print the results in a more readable format
167
- print("\nChunking Results:")
168
- print("----------------")
169
- for chunk in result["output"]:
170
- print(f"\nChunk #{chunk['seq_no']}:")
171
- print(f"Text: {chunk['text'][:100]}...") # Print first 100 chars of each chunk
172
- print(f"Source: {chunk['metadata']['source']}")
173
- print(f"Chunk ID: {chunk['chunk_id']}")