salesforce-data-customcode 6.0.3.dev1__tar.gz → 6.0.4__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.dev1 → salesforce_data_customcode-6.0.4}/PKG-INFO +5 -2
  2. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/README.md +4 -1
  3. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/pyproject.toml +2 -1
  4. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/cli.py +6 -3
  5. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/client.py +20 -8
  6. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/deploy.py +109 -19
  7. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/writer/base.py +11 -1
  8. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/spark_base.py +10 -1
  9. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/spark_default.py +70 -11
  10. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/run.py +15 -3
  11. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/.devcontainer/devcontainer.json +1 -1
  12. salesforce_data_customcode-6.0.4/src/datacustomcode/templates/function/payload/entrypoint.py +67 -0
  13. salesforce_data_customcode-6.0.4/src/datacustomcode/templates/function/payload/utility.py +104 -0
  14. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/.devcontainer/devcontainer.json +1 -1
  15. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/payload/entrypoint.py +4 -2
  16. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/token_provider.py +70 -36
  17. salesforce_data_customcode-6.0.3.dev1/src/datacustomcode/templates/function/payload/entrypoint.py +0 -173
  18. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/LICENSE.txt +0 -0
  19. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/__init__.py +0 -0
  20. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/auth.py +0 -0
  21. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/cmd.py +0 -0
  22. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/common_config.py +0 -0
  23. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/config.py +0 -0
  24. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/config.yaml +0 -0
  25. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/constants.py +0 -0
  26. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/credentials.py +0 -0
  27. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_platform_client.py +0 -0
  28. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_platform_config.py +0 -0
  29. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_predictions/__init__.py +0 -0
  30. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_predictions/base.py +0 -0
  31. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_predictions/impl/default.py +0 -0
  32. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_predictions/types.py +0 -0
  33. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/einstein_predictions_config.py +0 -0
  34. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/file/__init__.py +0 -0
  35. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/file/base.py +0 -0
  36. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/file/path/__init__.py +0 -0
  37. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/file/path/default.py +0 -0
  38. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/function/__init__.py +0 -0
  39. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/function/base.py +0 -0
  40. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/function/feature_types/__init__.py +0 -0
  41. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/function/feature_types/chunking.py +0 -0
  42. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/function/runtime.py +0 -0
  43. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/function_utils.py +0 -0
  44. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/__init__.py +0 -0
  45. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/base.py +0 -0
  46. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/reader/__init__.py +0 -0
  47. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/reader/base.py +0 -0
  48. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/reader/query_api.py +0 -0
  49. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/reader/sf_cli.py +0 -0
  50. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/reader/utils.py +0 -0
  51. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/writer/__init__.py +0 -0
  52. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/writer/csv.py +0 -0
  53. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/io/writer/print.py +0 -0
  54. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/__init__.py +0 -0
  55. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/base.py +0 -0
  56. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/default.py +0 -0
  57. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/errors.py +0 -0
  58. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/types/__init__.py +0 -0
  59. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/types/generate_text_request.py +0 -0
  60. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/types/generate_text_request_builder.py +0 -0
  61. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/types/generate_text_response.py +0 -0
  62. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway/types/generate_text_response_builder.py +0 -0
  63. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/llm_gateway_config.py +0 -0
  64. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/mixin.py +0 -0
  65. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/py.typed +0 -0
  66. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/scan.py +0 -0
  67. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/spark/__init__.py +0 -0
  68. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/spark/base.py +0 -0
  69. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/spark/default.py +0 -0
  70. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/template.py +0 -0
  71. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/__init__.py +0 -0
  72. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/Dockerfile.dependencies +0 -0
  73. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/README.md +0 -0
  74. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/build_native_dependencies.sh +0 -0
  75. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/chunking/payload/config.json +0 -0
  76. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/chunking/payload/entrypoint.py +0 -0
  77. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/chunking/requirements.txt +0 -0
  78. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_llm/config.json +0 -0
  79. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_llm/entrypoint.py +0 -0
  80. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_llm/files/chunking_prompt.txt +0 -0
  81. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_llm/tests/test.json +0 -0
  82. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_prediction/config.json +0 -0
  83. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_prediction/entrypoint.py +0 -0
  84. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/example/chunking_with_prediction/tests/test.json +0 -0
  85. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/payload/config.json +0 -0
  86. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/requirements-dev.txt +0 -0
  87. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/function/requirements.txt +0 -0
  88. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/Dockerfile +0 -0
  89. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/Dockerfile.dependencies +0 -0
  90. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/README.md +0 -0
  91. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/account.ipynb +0 -0
  92. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/build_native_dependencies.sh +0 -0
  93. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/examples/employee_hierarchy/employee_data.csv +0 -0
  94. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/examples/employee_hierarchy/entrypoint.py +0 -0
  95. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/jupyterlab.sh +0 -0
  96. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/payload/config.json +0 -0
  97. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/requirements-dev.txt +0 -0
  98. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/src/datacustomcode/templates/script/requirements.txt +0 -0
  99. {salesforce_data_customcode-6.0.3.dev1 → salesforce_data_customcode-6.0.4}/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.dev1
3
+ Version: 6.0.4
4
4
  Summary: Data Cloud Custom Code SDK
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE.txt
@@ -341,13 +341,16 @@ from datacustomcode.client import Client, llm_gateway_generate_text_col
341
341
  def main():
342
342
  client = Client()
343
343
  df = client.read_dlo("Input__dll")
344
+ # llm_gateway_generate_text_col returns a struct
345
+ # {status, response, error_code, error_message} per row, so per-row
346
+ # failures don't abort the Spark job. Pick the field you want with [].
344
347
  df_generated = df.withColumn(
345
348
  "greeting__c",
346
349
  llm_gateway_generate_text_col(
347
350
  "In one sentence, greet {name} from {city}.",
348
351
  {"name": col("name__c"), "city": col("homecity__c")},
349
352
  model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
350
- ),
353
+ )["response"],
351
354
  )
352
355
 
353
356
  dlo_name = "Output_dll"
@@ -316,13 +316,16 @@ from datacustomcode.client import Client, llm_gateway_generate_text_col
316
316
  def main():
317
317
  client = Client()
318
318
  df = client.read_dlo("Input__dll")
319
+ # llm_gateway_generate_text_col returns a struct
320
+ # {status, response, error_code, error_message} per row, so per-row
321
+ # failures don't abort the Spark job. Pick the field you want with [].
319
322
  df_generated = df.withColumn(
320
323
  "greeting__c",
321
324
  llm_gateway_generate_text_col(
322
325
  "In one sentence, greet {name} from {city}.",
323
326
  {"name": col("name__c"), "city": col("homecity__c")},
324
327
  model_id="sfdc_ai__DefaultGPT4Omni", # An AI model in your org
325
- ),
328
+ )["response"],
326
329
  )
327
330
 
328
331
  dlo_name = "Output_dll"
@@ -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.dev1"
21
+ version = "6.0.4"
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
  )
@@ -62,15 +62,23 @@ def llm_gateway_generate_text_col(
62
62
  ) -> "Column":
63
63
  """Build a Spark Column that runs the LLM Gateway per row.
64
64
 
65
+ The returned Column yields a struct ``{status, response, error_code,
66
+ error_message}`` for each row. Use ``[...]`` (or ``getField``) to pick the
67
+ field you want, e.g. ``llm_gateway_generate_text_col(...)["response"]``.
68
+ Per-row failures populate ``status`` / ``error_code`` / ``error_message``
69
+ so a single bad row does not abort the whole Spark job.
70
+
65
71
  Example:
66
72
 
67
- >>> df.withColumn(
68
- ... "greeting__c",
69
- ... llm_gateway_generate_text_col(
70
- ... "In one sentence, greet {name} from {city}.",
71
- ... {"name": col("name__c"), "city": col("homecity__c")},
72
- ... model_id="sfdc_ai__DefaultGPT4Omni",
73
- ... ),
73
+ >>> result = llm_gateway_generate_text_col(
74
+ ... "In one sentence, greet {name} from {city}.",
75
+ ... {"name": col("name__c"), "city": col("homecity__c")},
76
+ ... model_id="sfdc_ai__DefaultGPT4Omni",
77
+ ... )
78
+ >>> df.withColumn("greeting__c", result["response"])
79
+ >>> # …or keep the struct around and inspect failures:
80
+ >>> df.withColumn("llm", result).select(
81
+ ... "llm.status", "llm.response", "llm.error_message"
74
82
  ... )
75
83
 
76
84
  Args:
@@ -81,7 +89,11 @@ def llm_gateway_generate_text_col(
81
89
  model_id: LLM model id. Defaults to ``sfdc_ai__DefaultGPT4Omni``.
82
90
 
83
91
  Returns:
84
- A Spark ``Column`` that, when evaluated, produces the generated text.
92
+ A Spark ``Column`` of ``StructType`` with fields ``status``,
93
+ ``response``, ``error_code``, and ``error_message`` (all nullable
94
+ strings). On success, ``status == "SUCCESS"`` and ``response`` holds
95
+ the generated text; on failure, ``status == "ERROR"`` and the
96
+ ``error_*`` fields carry diagnostic detail.
85
97
  """
86
98
  gateway = Client()._get_spark_llm_gateway()
87
99
  return gateway.llm_gateway_generate_text_col(template, values, model_id=model_id)
@@ -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
 
@@ -50,4 +50,13 @@ class SparkLLMGateway(ABC, UserExtendableNamedConfigMixin):
50
50
  values: Union[Dict[str, "Column"], "Column"],
51
51
  model_id: Optional[str] = None,
52
52
  ) -> "Column":
53
- """Build a Spark ``Column`` that invokes the LLM Gateway per row."""
53
+ """Build a Spark ``Column`` that invokes the LLM Gateway per row and
54
+ yields a struct ``{status, response, error_code, error_message}``.
55
+
56
+ Select an individual field, e.g.
57
+ ``llm_gateway_generate_text_col(...)["response"]``. Returning a struct
58
+ means a single failing row doesn't abort the Spark job.
59
+ Failing row leaves the rest of the DataFrame intact — callers can
60
+ inspect ``status`` / ``error_code`` per row instead of having the
61
+ Spark job abort.
62
+ """
@@ -28,10 +28,16 @@ if TYPE_CHECKING:
28
28
  from pyspark.sql import Column
29
29
 
30
30
  from datacustomcode.llm_gateway.base import LLMGateway
31
+ from datacustomcode.llm_gateway.types.generate_text_response import (
32
+ GenerateTextResponse,
33
+ )
31
34
 
32
35
 
33
36
  _DEFAULT_LLM_MODEL_ID = "sfdc_ai__DefaultGPT4Omni"
34
37
 
38
+ _STATUS_SUCCESS = "SUCCESS"
39
+ _STATUS_ERROR = "ERROR"
40
+
35
41
 
36
42
  class DefaultSparkLLMGateway(SparkLLMGateway):
37
43
 
@@ -60,9 +66,17 @@ class DefaultSparkLLMGateway(SparkLLMGateway):
60
66
  values: Union[Dict[str, "Column"], "Column"],
61
67
  model_id: Optional[str] = None,
62
68
  ) -> "Column":
63
-
69
+ """Build a per-row UDF that returns a struct ``{status, response,
70
+ error_code, error_message}`` so per-row failures do not abort the
71
+ Spark job. Callers select the field they want, e.g.
72
+ ``llm_gateway_generate_text_col(...)["response"]``.
73
+ """
64
74
  from pyspark.sql.functions import struct, udf
65
- from pyspark.sql.types import StringType
75
+ from pyspark.sql.types import (
76
+ StringType,
77
+ StructField,
78
+ StructType,
79
+ )
66
80
 
67
81
  if isinstance(values, dict):
68
82
  values_col = struct(*[v.alias(k) for k, v in values.items()])
@@ -70,19 +84,32 @@ class DefaultSparkLLMGateway(SparkLLMGateway):
70
84
  values_col = values
71
85
 
72
86
  gateway = self._llm_gateway
87
+ result_schema = StructType(
88
+ [
89
+ StructField("status", StringType(), True),
90
+ StructField("response", StringType(), True),
91
+ StructField("error_code", StringType(), True),
92
+ StructField("error_message", StringType(), True),
93
+ ]
94
+ )
73
95
 
74
- def _generate(values_row: Any) -> str:
96
+ def _generate(values_row: Any) -> Dict[str, Optional[str]]:
75
97
  if values_row is None:
76
- return ""
98
+ return {
99
+ "status": _STATUS_ERROR,
100
+ "response": None,
101
+ "error_code": None,
102
+ "error_message": "values column was null for this row",
103
+ }
77
104
  subs = (
78
105
  values_row.asDict()
79
106
  if hasattr(values_row, "asDict")
80
107
  else dict(values_row)
81
108
  )
82
109
  prompt = template.format(**subs)
83
- return _invoke_llm_gateway(gateway, prompt, model_id)
110
+ return _invoke_llm_gateway_as_struct(gateway, prompt, model_id)
84
111
 
85
- return udf(_generate, StringType())(values_col)
112
+ return udf(_generate, result_schema)(values_col)
86
113
 
87
114
 
88
115
  def _build_underlying_gateway() -> "LLMGateway":
@@ -97,22 +124,33 @@ def _build_underlying_gateway() -> "LLMGateway":
97
124
  return cfg.to_object()
98
125
 
99
126
 
100
- def _invoke_llm_gateway(
127
+ def _call_llm_gateway(
101
128
  gateway: "LLMGateway",
102
129
  prompt: str,
103
130
  model_id: Optional[str],
104
- ) -> str:
105
- from datacustomcode.llm_gateway.errors import LLMGatewayCallError
131
+ ) -> "GenerateTextResponse":
132
+ """Build the request and dispatch it to the underlying gateway."""
106
133
  from datacustomcode.llm_gateway.types.generate_text_request_builder import (
107
134
  GenerateTextRequestBuilder,
108
135
  )
109
136
 
110
- builder = (
137
+ request = (
111
138
  GenerateTextRequestBuilder()
112
139
  .set_prompt(prompt)
113
140
  .set_model(model_id or _DEFAULT_LLM_MODEL_ID)
141
+ .build()
114
142
  )
115
- response = gateway.generate_text(builder.build())
143
+ return gateway.generate_text(request)
144
+
145
+
146
+ def _invoke_llm_gateway(
147
+ gateway: "LLMGateway",
148
+ prompt: str,
149
+ model_id: Optional[str],
150
+ ) -> str:
151
+ from datacustomcode.llm_gateway.errors import LLMGatewayCallError
152
+
153
+ response = _call_llm_gateway(gateway, prompt, model_id)
116
154
  if response.is_error:
117
155
  raise LLMGatewayCallError(
118
156
  f"LLM Gateway call failed: status_code={response.status_code}, "
@@ -123,3 +161,24 @@ def _invoke_llm_gateway(
123
161
  error_message=str(response.data) if response.data else None,
124
162
  )
125
163
  return response.text
164
+
165
+
166
+ def _invoke_llm_gateway_as_struct(
167
+ gateway: "LLMGateway",
168
+ prompt: str,
169
+ model_id: Optional[str],
170
+ ) -> Dict[str, Optional[str]]:
171
+ response = _call_llm_gateway(gateway, prompt, model_id)
172
+ if response.is_error:
173
+ return {
174
+ "status": _STATUS_ERROR,
175
+ "response": None,
176
+ "error_code": response.error_code or None,
177
+ "error_message": str(response.data) if response.data else None,
178
+ }
179
+ return {
180
+ "status": _STATUS_SUCCESS,
181
+ "response": response.text,
182
+ "error_code": None,
183
+ "error_message": None,
184
+ }
@@ -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)