Skip to content

Commit 827a352

Browse files
committed
pass output data object to transform api
1 parent 5336e10 commit 827a352

2 files changed

Lines changed: 288 additions & 5 deletions

File tree

src/datacustomcode/deploy.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Callable,
2727
Dict,
2828
List,
29+
Optional,
2930
Union,
3031
)
3132

@@ -366,10 +367,27 @@ class BaseConfig(BaseModel):
366367
entryPoint: str
367368

368369

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+
369386
class DataTransformConfig(BaseConfig):
370387
sdkVersion: str
371388
dataspace: str
372389
permissions: Permissions
390+
dataObjects: Optional[list[DataObject]] = None
373391

374392

375393
class FunctionConfig(BaseConfig):
@@ -409,6 +427,28 @@ def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
409427
return perm.dmo
410428

411429

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+
}
450+
451+
412452
def get_config(directory: str) -> BaseConfig:
413453
"""Get the code extension config from the config.json file."""
414454
config_path = os.path.join(directory, "config.json")
@@ -470,12 +510,27 @@ def create_data_transform(
470510

471511
request_hydrated["macros"]["macro.byoc"]["arguments"][0]["name"] = script_name
472512

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+
473532
body = {
474-
"definition": {
475-
"type": "DCSQL",
476-
"manifest": request_hydrated,
477-
"version": "56.0",
478-
},
533+
"definition": definition,
479534
"label": f"{metadata.name}",
480535
"name": f"{metadata.name}",
481536
"type": "BATCH",

tests/test_deploy.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import requests
1313

1414
from datacustomcode.deploy import (
15+
DataObject,
16+
DataObjectField,
1517
DloPermission,
1618
DmoPermission,
1719
Permissions,
@@ -985,6 +987,37 @@ def test_get_config_empty_permission_raises(self, mock_file):
985987
with pytest.raises(ValueError):
986988
get_config("/test/dir")
987989

990+
@patch(
991+
"builtins.open",
992+
new_callable=mock_open,
993+
read_data=(
994+
'{"sdkVersion": "0.1.14", "entryPoint": "entrypoint.py", '
995+
'"dataspace": "default", '
996+
'"permissions": {"read": {"dmo": ["ssot__Account__dlm"]}, '
997+
'"write": {"dmo": ["Account_DMO_Test__dlm"]}}, '
998+
'"dataObjects": [{'
999+
'"name": "Account_DMO_Test__dlm", '
1000+
'"label": "Account DMO Test", '
1001+
'"type": "dataModelObject", '
1002+
'"category": "profile", '
1003+
'"fields": [{"name": "Id__c", "label": "Account Id", '
1004+
'"dataType": "text", "isPrimaryKey": true, '
1005+
'"keyQualifierFieldName": "KQ_Id1__c"}]'
1006+
"}]}"
1007+
),
1008+
)
1009+
def test_get_config_dmo_with_data_objects(self, mock_file):
1010+
"""config.json parses the optional dataObjects schema for DMO writes."""
1011+
result = get_config("/test/dir")
1012+
assert isinstance(result, DataTransformConfig)
1013+
assert result.dataObjects is not None
1014+
assert len(result.dataObjects) == 1
1015+
obj = result.dataObjects[0]
1016+
assert obj.name == "Account_DMO_Test__dlm"
1017+
assert obj.category == "profile"
1018+
assert obj.fields[0].dataType == "text"
1019+
assert obj.fields[0].keyQualifierFieldName == "KQ_Id1__c"
1020+
9881021

9891022
class TestCreateDataTransform:
9901023
@patch("datacustomcode.deploy.get_config")
@@ -1059,6 +1092,23 @@ def test_create_data_transform_dmo(self, mock_make_api_call, mock_get_config):
10591092
read=DmoPermission(dmo=["input_dmo__dlm"]),
10601093
write=DmoPermission(dmo=["output_dmo__dlm"]),
10611094
),
1095+
dataObjects=[
1096+
DataObject(
1097+
name="output_dmo__dlm",
1098+
label="Output DMO",
1099+
type="dataModelObject",
1100+
category="profile",
1101+
fields=[
1102+
DataObjectField(
1103+
name="Id__c",
1104+
label="Id",
1105+
dataType="text",
1106+
isPrimaryKey=True,
1107+
keyQualifierFieldName="KQ_Id1__c",
1108+
)
1109+
],
1110+
)
1111+
],
10621112
)
10631113
mock_make_api_call.return_value = {"id": "transform_id"}
10641114

@@ -1104,6 +1154,23 @@ def test_create_data_transform_multiple_dmos(
11041154
read=DmoPermission(dmo=["in1__dlm", "in2__dlm"]),
11051155
write=DmoPermission(dmo=["out__dlm"]),
11061156
),
1157+
dataObjects=[
1158+
DataObject(
1159+
name="out__dlm",
1160+
label="Out",
1161+
type="dataModelObject",
1162+
category="profile",
1163+
fields=[
1164+
DataObjectField(
1165+
name="Id__c",
1166+
label="Id",
1167+
dataType="text",
1168+
isPrimaryKey=True,
1169+
keyQualifierFieldName="KQ_Id1__c",
1170+
)
1171+
],
1172+
)
1173+
],
11071174
)
11081175
mock_make_api_call.return_value = {"id": "transform_id"}
11091176

@@ -1125,6 +1192,167 @@ def test_create_data_transform_multiple_dmos(
11251192
}
11261193
}
11271194

1195+
@patch("datacustomcode.deploy.get_config")
1196+
@patch("datacustomcode.deploy._make_api_call")
1197+
def test_create_data_transform_dmo_emits_output_data_objects(
1198+
self, mock_make_api_call, mock_get_config
1199+
):
1200+
"""DMO transforms include outputDataObjects with transformed field names."""
1201+
access_token = AccessTokenResponse(
1202+
access_token="test_token", instance_url="https://instance.example.com"
1203+
)
1204+
metadata = CodeExtensionMetadata(
1205+
name="test_package",
1206+
version="1.0.0",
1207+
description="DMO with schema",
1208+
computeType="CPU_M",
1209+
codeType="script",
1210+
)
1211+
1212+
data_transform_config = DataTransformConfig(
1213+
sdkVersion="0.1.14",
1214+
entryPoint="entrypoint.py",
1215+
dataspace="default",
1216+
permissions=Permissions(
1217+
read=DmoPermission(dmo=["ssot__Account__dlm"]),
1218+
write=DmoPermission(dmo=["Account_DMO_Test__dlm"]),
1219+
),
1220+
dataObjects=[
1221+
DataObject(
1222+
name="Account_DMO_Test__dlm",
1223+
label="Account DMO Test",
1224+
type="dataModelObject",
1225+
category="profile",
1226+
fields=[
1227+
DataObjectField(
1228+
name="Id__c",
1229+
label="Account Id",
1230+
dataType="text",
1231+
isPrimaryKey=True,
1232+
keyQualifierFieldName="KQ_Id1__c",
1233+
),
1234+
DataObjectField(
1235+
name="KQ_Id1__c",
1236+
label="Key Qualifier Account Id",
1237+
dataType="text",
1238+
isPrimaryKey=False,
1239+
keyQualifierFieldName=None,
1240+
),
1241+
DataObjectField(
1242+
name="Description__c",
1243+
label="Account Description",
1244+
dataType="text",
1245+
isPrimaryKey=False,
1246+
keyQualifierFieldName=None,
1247+
),
1248+
],
1249+
)
1250+
],
1251+
)
1252+
mock_make_api_call.return_value = {"id": "transform_id"}
1253+
1254+
create_data_transform(
1255+
"/test/dir", access_token, metadata, data_transform_config
1256+
)
1257+
1258+
request_body = mock_make_api_call.call_args[1]["json"]
1259+
assert request_body["definition"]["outputDataObjects"] == [
1260+
{
1261+
"category": "profile",
1262+
"fields": [
1263+
{
1264+
"isPrimaryKey": True,
1265+
"keyQualifierField": "KQ_Id1__c",
1266+
"label": "Account Id",
1267+
"name": "Id__c",
1268+
"type": "text",
1269+
},
1270+
{
1271+
"isPrimaryKey": False,
1272+
"label": "Key Qualifier Account Id",
1273+
"name": "KQ_Id1__c",
1274+
"type": "text",
1275+
},
1276+
{
1277+
"isPrimaryKey": False,
1278+
"label": "Account Description",
1279+
"name": "Description__c",
1280+
"type": "text",
1281+
},
1282+
],
1283+
"label": "Account DMO Test",
1284+
"name": "Account_DMO_Test__dlm",
1285+
"type": "dataModelObject",
1286+
}
1287+
]
1288+
1289+
@patch("datacustomcode.deploy.get_config")
1290+
@patch("datacustomcode.deploy._make_api_call")
1291+
def test_create_data_transform_dlo_omits_output_data_objects(
1292+
self, mock_make_api_call, mock_get_config
1293+
):
1294+
"""DLO transforms must not include outputDataObjects in the payload."""
1295+
access_token = AccessTokenResponse(
1296+
access_token="test_token", instance_url="https://instance.example.com"
1297+
)
1298+
metadata = CodeExtensionMetadata(
1299+
name="dlo_job",
1300+
version="1.0.0",
1301+
description="DLO job",
1302+
computeType="CPU_M",
1303+
codeType="script",
1304+
)
1305+
1306+
data_transform_config = DataTransformConfig(
1307+
sdkVersion="1.0.0",
1308+
entryPoint="entrypoint.py",
1309+
dataspace="test_dataspace",
1310+
permissions=Permissions(
1311+
read=DloPermission(dlo=["input_dlo"]),
1312+
write=DloPermission(dlo=["output_dlo"]),
1313+
),
1314+
)
1315+
mock_make_api_call.return_value = {"id": "transform_id"}
1316+
1317+
create_data_transform(
1318+
"/test/dir", access_token, metadata, data_transform_config
1319+
)
1320+
1321+
request_body = mock_make_api_call.call_args[1]["json"]
1322+
assert "outputDataObjects" not in request_body["definition"]
1323+
1324+
@patch("datacustomcode.deploy.get_config")
1325+
@patch("datacustomcode.deploy._make_api_call")
1326+
def test_create_data_transform_dmo_missing_data_objects_raises(
1327+
self, mock_make_api_call, mock_get_config
1328+
):
1329+
"""DMO transforms without dataObjects raise a clear error."""
1330+
access_token = AccessTokenResponse(
1331+
access_token="test_token", instance_url="https://instance.example.com"
1332+
)
1333+
metadata = CodeExtensionMetadata(
1334+
name="dmo_no_schema",
1335+
version="1.0.0",
1336+
description="DMO no schema",
1337+
computeType="CPU_M",
1338+
codeType="script",
1339+
)
1340+
1341+
data_transform_config = DataTransformConfig(
1342+
sdkVersion="1.0.0",
1343+
entryPoint="entrypoint.py",
1344+
dataspace="test_dataspace",
1345+
permissions=Permissions(
1346+
read=DmoPermission(dmo=["input_dmo__dlm"]),
1347+
write=DmoPermission(dmo=["output_dmo__dlm"]),
1348+
),
1349+
)
1350+
1351+
with pytest.raises(ValueError, match="dataObjects"):
1352+
create_data_transform(
1353+
"/test/dir", access_token, metadata, data_transform_config
1354+
)
1355+
11281356

11291357
class TestDeployFull:
11301358
@patch("datacustomcode.deploy.get_config")

0 commit comments

Comments
 (0)