cumulusci-plus 5.0.17__py3-none-any.whl → 5.0.18__py3-none-any.whl

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.

Potentially problematic release.


This version of cumulusci-plus might be problematic. Click here for more details.

cumulusci/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "5.0.17"
1
+ __version__ = "5.0.18"
cumulusci/cumulusci.yml CHANGED
@@ -811,6 +811,22 @@ tasks:
811
811
  class_path: cumulusci.tasks.vlocity.vlocity.VlocityDeployTask
812
812
  description: "Executes the `vlocity packDeploy` command against an org"
813
813
  group: OmniStudio
814
+ data_query:
815
+ class_path: cumulusci.tasks.salesforce.SfDataCommands.DataQueryTask
816
+ description: "Executes the `sf data query` command against an org"
817
+ group: SalesforceDX Data Commands
818
+ data_delete:
819
+ class_path: cumulusci.tasks.salesforce.SfDataCommands.DataDeleteRecordTask
820
+ description: "Executes the `sf data delete` command against an org"
821
+ group: SalesforceDX Data Commands
822
+ data_create:
823
+ class_path: cumulusci.tasks.salesforce.SfDataCommands.DataCreateRecordTask
824
+ description: "Executes the `sf data create` command against an org"
825
+ group: SalesforceDX Data Commands
826
+ get_first_item_from_record_list:
827
+ class_path: cumulusci.tasks.utility.data_management.GetFirstItemFromRecordListTask
828
+ description: "Get the first item from a record list"
829
+ group: Utilities
814
830
  flows:
815
831
  ci_beta:
816
832
  group: Continuous Integration
@@ -0,0 +1,166 @@
1
+ import json
2
+
3
+ import sarge
4
+
5
+ from cumulusci.core.exceptions import SalesforceDXException
6
+ from cumulusci.core.sfdx import sfdx
7
+ from cumulusci.tasks.salesforce import BaseSalesforceApiTask
8
+ from cumulusci.utils.options import CCIOptions, Field
9
+
10
+
11
+ class SfDataCommands(BaseSalesforceApiTask):
12
+ class Options(CCIOptions):
13
+ json_output: bool = Field(
14
+ None, description="Whether to return the result as a JSON object"
15
+ )
16
+ api_version: str = Field(None, description="API version to use for the command")
17
+ flags_dir: str = Field(None, description="Import flag values from a directory")
18
+
19
+ parsed_options: Options
20
+
21
+ def _init_task(self):
22
+ super()._init_task()
23
+
24
+ def _init_options(self, kwargs):
25
+ self.args = []
26
+ self.data_command = "data "
27
+ super()._init_options(kwargs)
28
+ if self.parsed_options.flags_dir:
29
+ self.args.extend(["--flags-dir ", self.parsed_options.flags_dir])
30
+ if self.parsed_options.json_output:
31
+ self.args.extend(["--json"])
32
+ if self.parsed_options.api_version:
33
+ self.args.extend(["--api_version", self.parsed_options.api_version])
34
+
35
+ def _run_task(self):
36
+ self.return_values = {}
37
+
38
+ self.p: sarge.Command = sfdx(
39
+ self.data_command,
40
+ log_note="Running data command",
41
+ args=self.args,
42
+ check_return=True,
43
+ username=self.org_config.username,
44
+ )
45
+
46
+ if self.parsed_options.json_output:
47
+ self.return_values = self._load_json_output(self.p)
48
+
49
+ for line in self.p.stdout_text:
50
+ self.logger.info(line)
51
+
52
+ for line in self.p.stderr_text:
53
+ self.logger.error(line)
54
+
55
+ def _load_json_output(self, p: sarge.Command, stdout: str = None):
56
+ try:
57
+ stdout = stdout or p.stdout_text.read()
58
+ return json.loads(stdout)
59
+ except json.decoder.JSONDecodeError:
60
+ raise SalesforceDXException(
61
+ f"Failed to parse the output of the {self.data_command} command"
62
+ )
63
+
64
+
65
+ class SfDataToolingAPISupportedCommands(SfDataCommands):
66
+ class Options(SfDataCommands.Options):
67
+ use_tooling_api: bool = Field(
68
+ None,
69
+ description="Use Tooling API so you can run queries on Tooling API objects.",
70
+ )
71
+
72
+
73
+ class DataQueryTask(SfDataToolingAPISupportedCommands):
74
+ class Options(SfDataToolingAPISupportedCommands.Options):
75
+ query: str = Field(None, description="SOQL query to execute")
76
+ file: str = Field(None, description="File that contains the SOQL query")
77
+ all_rows: bool = Field(
78
+ None,
79
+ description="Include deleted records. By default, deleted records are not returned.",
80
+ )
81
+ result_format: str = Field(
82
+ None,
83
+ description="Format to display the results; the --json_output flag overrides this flag. Permissible values are: human, csv, json.",
84
+ )
85
+ output_file: str = Field(
86
+ None,
87
+ description="File where records are written; only CSV and JSON output formats are supported.",
88
+ )
89
+
90
+ def _init_task(self):
91
+ super()._init_task()
92
+ self.data_command += "query"
93
+
94
+ def _init_options(self, kwargs):
95
+ super()._init_options(kwargs)
96
+ if self.parsed_options.query:
97
+ self.args.extend(["--query", self.parsed_options.query])
98
+ if self.parsed_options.file:
99
+ self.args.extend(["--file", self.parsed_options.file])
100
+ if self.parsed_options.all_rows:
101
+ self.args.extend(["--all-rows", self.parsed_options.all_rows])
102
+ if self.parsed_options.result_format:
103
+ self.args.extend(["--result-format", self.parsed_options.result_format])
104
+ if self.parsed_options.output_file:
105
+ self.args.extend(["--output-file", self.parsed_options.output_file])
106
+
107
+ def _run_task(self):
108
+ super()._run_task()
109
+
110
+ if self.parsed_options.json_output:
111
+ self.logger.info(self.return_values)
112
+
113
+
114
+ class DataCreateRecordTask(SfDataToolingAPISupportedCommands):
115
+ class Options(SfDataToolingAPISupportedCommands.Options):
116
+ sobject: str = Field(
117
+ ...,
118
+ description="API name of the Salesforce or Tooling API object that you're inserting a record into.",
119
+ )
120
+ values: str = Field(
121
+ ...,
122
+ description="Values for the flags in the form <fieldName>=<value>, separate multiple pairs with spaces.",
123
+ )
124
+
125
+ def _init_task(self):
126
+ super()._init_task()
127
+ self.data_command += "create record"
128
+
129
+ def _init_options(self, kwargs):
130
+ super()._init_options(kwargs)
131
+ if self.options.get("sobject"):
132
+ self.args.extend(["--sobject", self.options.get("sobject")])
133
+ if self.options.get("values"):
134
+ self.args.extend(["--values", self.options.get("values")])
135
+
136
+ def _run_task(self):
137
+ return super()._run_task()
138
+
139
+
140
+ class DataDeleteRecordTask(SfDataToolingAPISupportedCommands):
141
+ class Options(SfDataToolingAPISupportedCommands.Options):
142
+ sobject: str = Field(
143
+ ...,
144
+ description="API name of the Salesforce or Tooling API object that you're deleting a record from.",
145
+ )
146
+ record_id: str = Field(None, description="ID of the record you’re deleting.")
147
+ where: str = Field(
148
+ None,
149
+ description="List of <fieldName>=<value> pairs that identify the record you want to delete.",
150
+ )
151
+
152
+ def _init_task(self):
153
+ super()._init_task()
154
+ self.data_command += "delete record"
155
+
156
+ def _init_options(self, kwargs):
157
+ super()._init_options(kwargs)
158
+ if self.options.get("sobject"):
159
+ self.args.extend(["--sobject", self.options.get("sobject")])
160
+ if self.options.get("record_id"):
161
+ self.args.extend(["--record-id", self.options.get("record_id")])
162
+ if self.options.get("where"):
163
+ self.args.extend(["--where", self.options.get("where")])
164
+
165
+ def _run_task(self):
166
+ return super()._run_task()
@@ -0,0 +1,363 @@
1
+ import json
2
+ from unittest import mock
3
+
4
+ import pytest
5
+ import sarge
6
+
7
+ from cumulusci.core.exceptions import SalesforceDXException
8
+ from cumulusci.tasks.salesforce.SfDataCommands import (
9
+ DataCreateRecordTask,
10
+ DataDeleteRecordTask,
11
+ DataQueryTask,
12
+ SfDataCommands,
13
+ SfDataToolingAPISupportedCommands,
14
+ )
15
+
16
+ from . import create_task
17
+
18
+
19
+ def create_mock_sarge_command(stdout="", stderr="", returncode=0):
20
+ """Create a mock sarge.Command that satisfies type checking"""
21
+ mock_command = mock.Mock(spec=sarge.Command)
22
+ mock_command.returncode = returncode
23
+
24
+ # Mock stdout_text
25
+ stdout_lines = stdout.split("\n") if stdout else []
26
+ mock_stdout_text = mock.Mock()
27
+ mock_stdout_text.__iter__ = mock.Mock(return_value=iter(stdout_lines))
28
+ mock_stdout_text.read = mock.Mock(return_value=stdout)
29
+ mock_command.stdout_text = mock_stdout_text
30
+
31
+ # Mock stderr_text
32
+ stderr_lines = stderr.split("\n") if stderr else []
33
+ mock_stderr_text = mock.Mock()
34
+ mock_stderr_text.__iter__ = mock.Mock(return_value=iter(stderr_lines))
35
+ mock_stderr_text.read = mock.Mock(return_value=stderr)
36
+ mock_command.stderr_text = mock_stderr_text
37
+
38
+ return mock_command
39
+
40
+
41
+ @mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
42
+ class TestSfDataCommands:
43
+ def test_init_options_basic(self, mock_sfdx):
44
+ mock_sfdx.return_value = create_mock_sarge_command()
45
+ task = create_task(SfDataCommands, {})
46
+ task()
47
+ assert task.data_command == "data "
48
+ assert task.args == []
49
+
50
+ def test_init_options_all_flags(self, mock_sfdx):
51
+ json_response = {"status": 0, "result": {}}
52
+ mock_sfdx.return_value = create_mock_sarge_command(
53
+ stdout=json.dumps(json_response)
54
+ )
55
+ task = create_task(
56
+ SfDataCommands,
57
+ {
58
+ "json_output": True,
59
+ "api_version": "50.0",
60
+ "flags_dir": "/tmp/flags",
61
+ },
62
+ )
63
+ task()
64
+ expected_args = [
65
+ "--flags-dir ",
66
+ "/tmp/flags",
67
+ "--json",
68
+ "--api_version",
69
+ "50.0",
70
+ ]
71
+ assert all(arg in task.args for arg in expected_args)
72
+
73
+ def test_load_json_output_success(self, mock_sfdx):
74
+ json_response = {"status": 0, "result": {"records": []}}
75
+ stdout = json.dumps(json_response)
76
+ mock_command = create_mock_sarge_command(stdout=stdout)
77
+ mock_sfdx.return_value = mock_command
78
+
79
+ task = create_task(SfDataCommands, {"json_output": True})
80
+ task()
81
+
82
+ assert task.return_values == json_response
83
+
84
+ def test_load_json_output_decode_error(self, mock_sfdx):
85
+ mock_command = create_mock_sarge_command(stdout="invalid json")
86
+ mock_sfdx.return_value = mock_command
87
+
88
+ task = create_task(SfDataCommands, {"json_output": True})
89
+
90
+ with pytest.raises(SalesforceDXException, match="Failed to parse the output"):
91
+ task()
92
+
93
+ def test_logging_stdout_and_stderr(self, mock_sfdx):
94
+ mock_command = create_mock_sarge_command(
95
+ stdout="Success: Query completed\nFound 5 records",
96
+ stderr="Warning: Some non-critical issue",
97
+ )
98
+ mock_sfdx.return_value = mock_command
99
+
100
+ task = create_task(SfDataCommands, {})
101
+ with mock.patch.object(task, "logger") as mock_logger:
102
+ task()
103
+
104
+ # Check that stdout lines were logged as info
105
+ mock_logger.info.assert_any_call("Success: Query completed")
106
+ mock_logger.info.assert_any_call("Found 5 records")
107
+
108
+ # Check that stderr lines were logged as error
109
+ mock_logger.error.assert_called_with("Warning: Some non-critical issue")
110
+
111
+
112
+ @mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
113
+ class TestSfDataToolingAPISupportedCommands:
114
+ def test_inherits_from_base(self, mock_sfdx):
115
+ mock_sfdx.return_value = create_mock_sarge_command()
116
+ task = create_task(SfDataToolingAPISupportedCommands, {})
117
+ assert isinstance(task, SfDataCommands)
118
+
119
+ def test_use_tooling_api_option(self, mock_sfdx):
120
+ mock_sfdx.return_value = create_mock_sarge_command()
121
+ task = create_task(SfDataToolingAPISupportedCommands, {"use_tooling_api": True})
122
+ # Test that the option is available (would be tested in subclasses that use it)
123
+ assert hasattr(task.parsed_options, "use_tooling_api")
124
+
125
+
126
+ @mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
127
+ class TestDataQueryTask:
128
+ def test_init_task_sets_command(self, mock_sfdx):
129
+ mock_sfdx.return_value = create_mock_sarge_command()
130
+ task = create_task(DataQueryTask, {"query": "SELECT Id FROM Account"})
131
+ task()
132
+ assert task.data_command == "data query"
133
+
134
+ def test_init_options_all_parameters(self, mock_sfdx):
135
+ json_response = {"status": 0, "result": {"records": []}}
136
+ mock_sfdx.return_value = create_mock_sarge_command(
137
+ stdout=json.dumps(json_response)
138
+ )
139
+ task = create_task(
140
+ DataQueryTask,
141
+ {
142
+ "query": "SELECT Id FROM Account",
143
+ "file": "/tmp/query.soql",
144
+ "all_rows": True,
145
+ "result_format": "csv",
146
+ "output_file": "/tmp/output.csv",
147
+ "api_version": "50.0",
148
+ "flags_dir": "/tmp/flags",
149
+ "json_output": True,
150
+ "use_tooling_api": True,
151
+ },
152
+ )
153
+ task()
154
+
155
+ call_args = mock_sfdx.call_args[1]["args"]
156
+ assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
157
+ assert "--query" in call_args
158
+ assert "SELECT Id FROM Account" in call_args
159
+ assert "--file" in call_args
160
+ assert "/tmp/query.soql" in call_args
161
+ assert "--all-rows" in call_args
162
+ assert "--result-format" in call_args
163
+ assert "csv" in call_args
164
+ assert "--output-file" in call_args
165
+ assert "/tmp/output.csv" in call_args
166
+ assert "--api_version" in call_args
167
+ assert "50.0" in call_args
168
+ assert "--flags-dir " in call_args
169
+ assert "/tmp/flags" in call_args
170
+ assert "--json" in call_args
171
+
172
+ def test_minimal_options(self, mock_sfdx):
173
+ mock_sfdx.return_value = create_mock_sarge_command()
174
+ task = create_task(DataQueryTask, {"query": "SELECT Id FROM Account"})
175
+ task()
176
+
177
+ call_args = mock_sfdx.call_args[1]["args"]
178
+ assert "--query" in call_args
179
+ assert "SELECT Id FROM Account" in call_args
180
+
181
+ def test_json_output_logging(self, mock_sfdx):
182
+ json_response = {"status": 0, "result": {"records": [{"Id": "123"}]}}
183
+ stdout = json.dumps(json_response)
184
+ mock_sfdx.return_value = create_mock_sarge_command(stdout=stdout)
185
+
186
+ task = create_task(
187
+ DataQueryTask,
188
+ {"query": "SELECT Id FROM Account", "json_output": True},
189
+ )
190
+
191
+ with mock.patch.object(task, "logger") as mock_logger:
192
+ task()
193
+ mock_logger.info.assert_called_with(json_response)
194
+
195
+ def test_run_task_json_decode_error(self, mock_sfdx):
196
+ mock_sfdx.return_value = create_mock_sarge_command(stdout="this is not json")
197
+ task = create_task(
198
+ DataQueryTask,
199
+ {"query": "SELECT Id FROM Account", "json_output": True},
200
+ )
201
+ with pytest.raises(
202
+ SalesforceDXException,
203
+ match="Failed to parse the output of the data query command",
204
+ ):
205
+ task()
206
+
207
+
208
+ @mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
209
+ class TestDataCreateRecordTask:
210
+ def test_init_task_sets_command(self, mock_sfdx):
211
+ mock_sfdx.return_value = create_mock_sarge_command()
212
+ task = create_task(
213
+ DataCreateRecordTask,
214
+ {"sobject": "Account", "values": "Name='Test Account'"},
215
+ )
216
+ task()
217
+ assert task.data_command == "data create record"
218
+
219
+ def test_init_options_all_parameters(self, mock_sfdx):
220
+ json_response = {"status": 0, "result": {"id": "001000000000001"}}
221
+ mock_sfdx.return_value = create_mock_sarge_command(
222
+ stdout=json.dumps(json_response)
223
+ )
224
+ task = create_task(
225
+ DataCreateRecordTask,
226
+ {
227
+ "sobject": "Account",
228
+ "values": "Name='Test Account' Industry='Technology'",
229
+ "api_version": "50.0",
230
+ "flags_dir": "/tmp/flags",
231
+ "json_output": True,
232
+ "use_tooling_api": True,
233
+ },
234
+ )
235
+ task()
236
+
237
+ call_args = mock_sfdx.call_args[1]["args"]
238
+ assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
239
+ assert "--sobject" in call_args
240
+ assert "Account" in call_args
241
+ assert "--values" in call_args
242
+ assert "Name='Test Account' Industry='Technology'" in call_args
243
+ assert "--api_version" in call_args
244
+ assert "50.0" in call_args
245
+ assert "--flags-dir " in call_args
246
+ assert "/tmp/flags" in call_args
247
+ assert "--json" in call_args
248
+
249
+ def test_minimal_options(self, mock_sfdx):
250
+ mock_sfdx.return_value = create_mock_sarge_command()
251
+ task = create_task(
252
+ DataCreateRecordTask,
253
+ {"sobject": "Contact", "values": "LastName='Doe'"},
254
+ )
255
+ task()
256
+
257
+ call_args = mock_sfdx.call_args[1]["args"]
258
+ assert "--sobject" in call_args
259
+ assert "Contact" in call_args
260
+ assert "--values" in call_args
261
+ assert "LastName='Doe'" in call_args
262
+
263
+ def test_missing_required_options(self, mock_sfdx):
264
+ # Test that required field validation works as expected
265
+ mock_sfdx.return_value = create_mock_sarge_command()
266
+ from cumulusci.core.exceptions import TaskOptionsError
267
+
268
+ with pytest.raises(TaskOptionsError, match="field required"):
269
+ create_task(DataCreateRecordTask, {})
270
+
271
+
272
+ @mock.patch("cumulusci.tasks.salesforce.SfDataCommands.sfdx")
273
+ class TestDataDeleteRecordTask:
274
+ def test_init_task_sets_command(self, mock_sfdx):
275
+ mock_sfdx.return_value = create_mock_sarge_command()
276
+ task = create_task(
277
+ DataDeleteRecordTask, {"sobject": "Account", "record_id": "001000000000001"}
278
+ )
279
+ task()
280
+ assert task.data_command == "data delete record"
281
+
282
+ def test_delete_by_record_id(self, mock_sfdx):
283
+ json_response = {"status": 0, "result": {}}
284
+ mock_sfdx.return_value = create_mock_sarge_command(
285
+ stdout=json.dumps(json_response)
286
+ )
287
+ task = create_task(
288
+ DataDeleteRecordTask,
289
+ {
290
+ "sobject": "Account",
291
+ "record_id": "001000000000001AAA",
292
+ "api_version": "50.0",
293
+ "flags_dir": "/tmp/flags",
294
+ "json_output": True,
295
+ },
296
+ )
297
+ task()
298
+
299
+ call_args = mock_sfdx.call_args[1]["args"]
300
+ assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
301
+ assert "--sobject" in call_args
302
+ assert "Account" in call_args
303
+ assert "--record-id" in call_args
304
+ assert "001000000000001AAA" in call_args
305
+ assert "--api_version" in call_args
306
+ assert "50.0" in call_args
307
+ assert "--flags-dir " in call_args
308
+ assert "/tmp/flags" in call_args
309
+ assert "--json" in call_args
310
+
311
+ def test_delete_by_where_clause(self, mock_sfdx):
312
+ mock_sfdx.return_value = create_mock_sarge_command()
313
+ task = create_task(
314
+ DataDeleteRecordTask,
315
+ {
316
+ "sobject": "Account",
317
+ "where": "Name='Test Account' AND Industry='Technology'",
318
+ "use_tooling_api": True,
319
+ },
320
+ )
321
+ task()
322
+
323
+ call_args = mock_sfdx.call_args[1]["args"]
324
+ # Check that sfdx was called with the right log_note
325
+ assert mock_sfdx.call_args[1]["log_note"] == "Running data command"
326
+ assert "--sobject" in call_args
327
+ assert "Account" in call_args
328
+ assert "--where" in call_args
329
+ assert "Name='Test Account' AND Industry='Technology'" in call_args
330
+
331
+ def test_minimal_options(self, mock_sfdx):
332
+ mock_sfdx.return_value = create_mock_sarge_command()
333
+ task = create_task(
334
+ DataDeleteRecordTask,
335
+ {"sobject": "Contact"},
336
+ )
337
+ task()
338
+
339
+ call_args = mock_sfdx.call_args[1]["args"]
340
+ assert "--sobject" in call_args
341
+ assert "Contact" in call_args
342
+ # Should not have --record-id or --where if not provided
343
+ assert "--record-id" not in call_args
344
+ assert "--where" not in call_args
345
+
346
+ def test_both_record_id_and_where_provided(self, mock_sfdx):
347
+ # Test that both options can be provided (though this might not be practical)
348
+ mock_sfdx.return_value = create_mock_sarge_command()
349
+ task = create_task(
350
+ DataDeleteRecordTask,
351
+ {
352
+ "sobject": "Account",
353
+ "record_id": "001000000000001",
354
+ "where": "Name='Test Account'",
355
+ },
356
+ )
357
+ task()
358
+
359
+ call_args = mock_sfdx.call_args[1]["args"]
360
+ assert "--record-id" in call_args
361
+ assert "001000000000001" in call_args
362
+ assert "--where" in call_args
363
+ assert "Name='Test Account'" in call_args
@@ -0,0 +1,16 @@
1
+ from typing import Any
2
+
3
+ from cumulusci.core.tasks import BaseTask
4
+ from cumulusci.utils.options import CCIOptions, Field
5
+
6
+
7
+ class GetFirstItemFromRecordListTask(BaseTask):
8
+ class Options(CCIOptions):
9
+ result: dict[str, Any] = Field(
10
+ None, description="The result from SF CLI operations."
11
+ )
12
+
13
+ parsed_options: Options
14
+
15
+ def _run_task(self):
16
+ self.return_values = self.parsed_options.result["records"][0]
@@ -109,14 +109,10 @@ class EnvManagementOption(CCIOptions):
109
109
  f"Formatting Error: {value} for datatype: {datatype} - {e}"
110
110
  )
111
111
 
112
- if self.set and self.name not in os.environ:
112
+ if self.set:
113
113
  os.environ[self.name] = str(task_values[self.name])
114
114
 
115
- if (
116
- self.set
117
- and self.datatype == "vcs_repo"
118
- and f"{self.name}_BRANCH" not in os.environ
119
- ):
115
+ if self.set and self.datatype == "vcs_repo":
120
116
  os.environ[f"{self.name}_BRANCH"] = str(task_values[f"{self.name}_BRANCH"])
121
117
 
122
118
 
@@ -0,0 +1,43 @@
1
+ import pytest
2
+
3
+ from cumulusci.core.config import (
4
+ BaseProjectConfig,
5
+ OrgConfig,
6
+ TaskConfig,
7
+ UniversalConfig,
8
+ )
9
+ from cumulusci.tasks.utility.data_management import GetFirstItemFromRecordListTask
10
+
11
+
12
+ @pytest.fixture
13
+ def a_task():
14
+ universal_config = UniversalConfig()
15
+ project_config = BaseProjectConfig(universal_config, config={"noyaml": True})
16
+ org_config = OrgConfig({}, "test")
17
+
18
+ def _a_task(options):
19
+ task_config = TaskConfig({"options": options})
20
+ return GetFirstItemFromRecordListTask(project_config, task_config, org_config)
21
+
22
+ return _a_task
23
+
24
+
25
+ class TestGetFirstItemFromRecordListTask:
26
+ def test_returns_first_item(self, a_task):
27
+ """Tests that the task returns the first item from the 'records' list."""
28
+ result_input = {
29
+ "records": [
30
+ {"Id": "001", "Name": "Test Account"},
31
+ {"Id": "002", "Name": "Another Account"},
32
+ ]
33
+ }
34
+ task = a_task({"result": result_input})
35
+ task()
36
+ assert task.return_values == {"Id": "001", "Name": "Test Account"}
37
+
38
+ def test_empty_record_list(self, a_task):
39
+ """Tests that an IndexError is raised for an empty 'records' list."""
40
+ result_input = {"records": []}
41
+ task = a_task({"result": result_input})
42
+ with pytest.raises(IndexError):
43
+ task()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cumulusci-plus
3
- Version: 5.0.17
3
+ Version: 5.0.18
4
4
  Summary: Build and release tools for Salesforce developers
5
5
  Project-URL: Homepage, https://github.com/jorgesolebur/CumulusCI
6
6
  Project-URL: Changelog, https://cumulusci.readthedocs.io/en/stable/history.html
@@ -127,7 +127,7 @@ license](https://github.com/SFDO-Tooling/CumulusCI/blob/main/LICENSE)
127
127
  and is not covered by the Salesforce Master Subscription Agreement.
128
128
 
129
129
  <!-- Changelog -->
130
- ## v5.0.17 (2025-08-21)
130
+ ## v5.0.18 (2025-08-23)
131
131
 
132
132
  <!-- Release notes generated using configuration in .github/release.yml at main -->
133
133
 
@@ -135,6 +135,6 @@ and is not covered by the Salesforce Master Subscription Agreement.
135
135
 
136
136
  ### Changes
137
137
 
138
- - Add custom data type vcs_repo. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#59](https://github.com/jorgesolebur/CumulusCI/pull/59)
138
+ - Add sf data command of create, query and delete. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#61](https://github.com/jorgesolebur/CumulusCI/pull/61)
139
139
 
140
- **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.16...v5.0.17
140
+ **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.17...v5.0.18
@@ -1,8 +1,8 @@
1
- cumulusci/__about__.py,sha256=x4NllGN1pNABBzi1vkIo-rQ2JSdgVyJUAI6uFT43BEA,23
1
+ cumulusci/__about__.py,sha256=rNEejWvxeGfAnWPU00R1oUZaWa42-NuQA6ta50JVBY0,23
2
2
  cumulusci/__init__.py,sha256=jdanFQ_i8vbdO7Eltsf4pOfvV4mwa_Osyc4gxWKJ8ng,764
3
3
  cumulusci/__main__.py,sha256=kgRH-n5AJrH_daCK_EJwH7azAUxdXEmpi-r-dPGMR6Y,43
4
4
  cumulusci/conftest.py,sha256=AIL98BDwNAQtdo8YFmLKwav0tmrQ5dpbw1cX2FyGouQ,5108
5
- cumulusci/cumulusci.yml,sha256=h4Ywck_HV5PXqjdTvA6jMm8dxPLZi2tmFdwGmssnhdo,72617
5
+ cumulusci/cumulusci.yml,sha256=RQN4mskRwSYaP33g-b9SZg8j62rU5ynpCE7ZBxfDqFY,73476
6
6
  cumulusci/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  cumulusci/cli/cci.py,sha256=yAq8jFoGde6g_1TeAAjzZYsk77itiONCQGBFe3g3nOs,11836
8
8
  cumulusci/cli/error.py,sha256=znj0YN8D2Grozm1u7mZAsJlmmdGebbuy0c1ofQluL4Q,4410
@@ -516,6 +516,7 @@ cumulusci/tasks/salesforce/RetrievePackaged.py,sha256=XMdA3qBIuGJaZnPNeZBKupz9N6
516
516
  cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py,sha256=-pVD0RJ4vsB1TM9b7VcPQAV-pO4LTAm-YoESq4bM2dA,3289
517
517
  cumulusci/tasks/salesforce/RetrieveUnpackaged.py,sha256=_9jglbNWmjg9kq5EkvCusprrt14czurBtNM2hbU9RFI,1174
518
518
  cumulusci/tasks/salesforce/SOQLQuery.py,sha256=RFV-bLzvAx_m4SWWLMyCKuA3ie08iLwLyrOVyhpNCPE,1730
519
+ cumulusci/tasks/salesforce/SfDataCommands.py,sha256=tCrjvxZDk4vrVmeJo7BR5uTEZUUbhbAGNoRuwt4zn1k,6094
519
520
  cumulusci/tasks/salesforce/UninstallLocal.py,sha256=6nnRDyFrwIcXd9WygDHIMhMQYBZMFEQ75Sh6nNxivfg,492
520
521
  cumulusci/tasks/salesforce/UninstallLocalBundles.py,sha256=eG4yoDJdjruo9kwfcQU-i5_AVt473uWF7SvLD7IQoSw,759
521
522
  cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py,sha256=KgM6TPSkczAdeM10fOAtijs4uszJ_f219lGgVNwJ1eM,2210
@@ -561,6 +562,7 @@ cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py,sha256=jhXYXv6I54HUfOf
561
562
  cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py,sha256=6ePEXIqMdNLKcnxUkwnpWSqJ1TaWFfAcl3lEhbsqFFI,1730
562
563
  cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py,sha256=mXMcw0a1til3R8eMm7KGZ5oftqKT5XLFoZ047UAO0Z8,668
563
564
  cumulusci/tasks/salesforce/tests/test_SOQLQuery.py,sha256=YHsdSzFNEY8R5qbT0al9I0mYHWNT8P4hqYji2gDVnEU,849
565
+ cumulusci/tasks/salesforce/tests/test_SfDataCommands.py,sha256=JdM7P-lDXKtvxK4UlHMOIfzCVz6uH2eaIyyJEZKenuk,13353
564
566
  cumulusci/tasks/salesforce/tests/test_UninstallLocal.py,sha256=v7UtmVPFGeH13mSnuZi8eI5vaDeO7WlDu1bhM3s9-fk,582
565
567
  cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py,sha256=oz8xuYGda4aYXskIAB0mUAJpgroOhV9Ahb-a4oNJtio,557
566
568
  cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py,sha256=oj_OpxDnTRSX782aWSGsttnybrO3cZh3Mtk_NiLZiOs,907
@@ -611,7 +613,9 @@ cumulusci/tasks/tests/test_pushfails.py,sha256=9JG9D0iD4dR-1fKheaRN7BEy3lzzuOKeR
611
613
  cumulusci/tasks/tests/test_salesforce.py,sha256=yCGtuHapxyAEmXQhuF2g2fh2naknTu7Md4OfEJQvGAA,2594
612
614
  cumulusci/tasks/tests/test_sfdx.py,sha256=oUbHo28d796m5RuskXMLitJw2rCLjjXIfxggzr4gsso,3545
613
615
  cumulusci/tasks/tests/test_util.py,sha256=D1T0QnvPTS0PHeZWo2xiVgE1jVTYcLzGTGHwEIoVmxk,7296
614
- cumulusci/tasks/utility/env_management.py,sha256=nJyv7KFtaBaAUK_40h-FpyfHTd7xSfEFkm9cj-tOhqc,6715
616
+ cumulusci/tasks/utility/data_management.py,sha256=7DbK0uxMxj_e4WuH08gWFVxr_ZBMrvHvumHnM_oi2YM,443
617
+ cumulusci/tasks/utility/env_management.py,sha256=hJX6ySEiXD2oFW38JqbGQKMj89ucxdSBsPwytSdkgO8,6591
618
+ cumulusci/tasks/utility/tests/test_data_management.py,sha256=f3F1yFikaNEkzllMXZa67gNX0T_PCC-4zEoL8kUII_Q,1363
615
619
  cumulusci/tasks/utility/tests/test_env_management.py,sha256=fw34meWGOe1YYZO449MMCi2O7BgSaOA_I_wScrIr1Uk,8702
616
620
  cumulusci/tasks/vcs/__init__.py,sha256=ZzpMZnhooXZ6r_ywBVTS3UNw9uMcXW6h33LylRqTDK0,700
617
621
  cumulusci/tasks/vcs/commit_status.py,sha256=hgPUVHeQyIfMsCuwrw2RI-ufnbjdRARc6HI3BEgdcxI,2332
@@ -738,9 +742,9 @@ cumulusci/vcs/tests/dummy_service.py,sha256=RltOUpMIhSDNrfxk0LhLqlH4ppC0sK6NC2cO
738
742
  cumulusci/vcs/tests/test_vcs_base.py,sha256=9mp6uZ3lTxY4onjUNCucp9N9aB3UylKS7_2Zu_hdAZw,24331
739
743
  cumulusci/vcs/tests/test_vcs_bootstrap.py,sha256=N0NA48-rGNIIjY3Z7PtVnNwHObSlEGDk2K55TQGI8g4,27954
740
744
  cumulusci/vcs/utils/__init__.py,sha256=py4fEcHM7Vd0M0XWznOlywxaeCtG3nEVGmELmEKVGU8,869
741
- cumulusci_plus-5.0.17.dist-info/METADATA,sha256=hO8FAqzbbv4M0ugIDh2QHRGmg7WArDbaCBGrPgW4Vto,5751
742
- cumulusci_plus-5.0.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
743
- cumulusci_plus-5.0.17.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
744
- cumulusci_plus-5.0.17.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
745
- cumulusci_plus-5.0.17.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
746
- cumulusci_plus-5.0.17.dist-info/RECORD,,
745
+ cumulusci_plus-5.0.18.dist-info/METADATA,sha256=EjWQpoxW3IMvVViclvXJR9WyKkDlsnxTxZrsX6m0Zwk,5769
746
+ cumulusci_plus-5.0.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
747
+ cumulusci_plus-5.0.18.dist-info/entry_points.txt,sha256=nTtu04b9iLXhzADcTrb5PwmdXE6e2MTUAMh9OK6Z2pg,80
748
+ cumulusci_plus-5.0.18.dist-info/licenses/AUTHORS.rst,sha256=PvewjKImdKPhhJ6xR2EEZ4T7GbpY2ZeAeyWm2aLtiMQ,676
749
+ cumulusci_plus-5.0.18.dist-info/licenses/LICENSE,sha256=NFsF_s7RVXk2dU6tmRAN8wF45pnD98VZ5IwqOsyBcaU,1499
750
+ cumulusci_plus-5.0.18.dist-info/RECORD,,