cumulusci-plus 5.0.18__py3-none-any.whl → 5.0.20__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.

@@ -164,3 +164,237 @@ class DataDeleteRecordTask(SfDataToolingAPISupportedCommands):
164
164
 
165
165
  def _run_task(self):
166
166
  return super()._run_task()
167
+
168
+
169
+ class DataUpdateRecordTask(SfDataToolingAPISupportedCommands):
170
+ class Options(SfDataToolingAPISupportedCommands.Options):
171
+ sobject: str = Field(
172
+ ...,
173
+ description="API name of the Salesforce or Tooling API object that you're updating a record from.",
174
+ )
175
+ record_id: str = Field(None, description="ID of the record you’re updating.")
176
+ where: str = Field(
177
+ None,
178
+ description="List of <fieldName>=<value> pairs that identify the record you want to update.",
179
+ )
180
+ values: str = Field(
181
+ ...,
182
+ description="Values for the flags in the form <fieldName>=<value>, separate multiple pairs with spaces.",
183
+ )
184
+
185
+ def _init_task(self):
186
+ super()._init_task()
187
+ self.data_command += "update record"
188
+
189
+ def _init_options(self, kwargs):
190
+ super()._init_options(kwargs)
191
+ if self.parsed_options.sobject:
192
+ self.args.extend(["--sobject", self.parsed_options.sobject])
193
+ if self.parsed_options.record_id:
194
+ self.args.extend(["--record-id", self.parsed_options.record_id])
195
+ if self.parsed_options.where:
196
+ self.args.extend(["--where", self.parsed_options.where])
197
+ if self.parsed_options.values:
198
+ self.args.extend(["--values", self.parsed_options.values])
199
+
200
+ def _run_task(self):
201
+ return super()._run_task()
202
+
203
+
204
+ class DataGetRecordTask(SfDataToolingAPISupportedCommands):
205
+ class Options(SfDataToolingAPISupportedCommands.Options):
206
+ sobject: str = Field(
207
+ ...,
208
+ description="API name of the Salesforce or Tooling API object that you're fetching a record from.",
209
+ )
210
+ record_id: str = Field(None, description="ID of the record you’re fetching.")
211
+ where: str = Field(
212
+ None,
213
+ description="List of <fieldName>=<value> pairs that identify the record you want to fetch.",
214
+ )
215
+
216
+ def _init_task(self):
217
+ super()._init_task()
218
+ self.data_command += "get record"
219
+
220
+ def _init_options(self, kwargs):
221
+ super()._init_options(kwargs)
222
+ if self.parsed_options.sobject:
223
+ self.args.extend(["--sobject", self.parsed_options.sobject])
224
+ if self.parsed_options.record_id:
225
+ self.args.extend(["--record-id", self.parsed_options.record_id])
226
+ if self.parsed_options.where:
227
+ self.args.extend(["--where", self.parsed_options.where])
228
+
229
+ def _run_task(self):
230
+ return super()._run_task()
231
+
232
+
233
+ class DataQueryResumeTask(SfDataToolingAPISupportedCommands):
234
+ class Options(SfDataToolingAPISupportedCommands.Options):
235
+ bulk_query_id: str = Field(
236
+ ...,
237
+ description="The 18-character ID of the bulk query to resume.",
238
+ )
239
+ result_format: str = Field(
240
+ None,
241
+ description="Format to display the results; the --json_output flag overrides this flag. Permissible values are: human, csv, json.",
242
+ )
243
+
244
+ def _init_task(self):
245
+ super()._init_task()
246
+ self.data_command += "query resume"
247
+
248
+ def _init_options(self, kwargs):
249
+ super()._init_options(kwargs)
250
+ if self.parsed_options.bulk_query_id:
251
+ self.args.extend(["--bulk-query-id", self.parsed_options.bulk_query_id])
252
+ if self.parsed_options.result_format:
253
+ self.args.extend(["--result-format", self.parsed_options.result_format])
254
+
255
+ def _run_task(self):
256
+ return super()._run_task()
257
+
258
+
259
+ class DataDeleteBulkTask(SfDataCommands):
260
+ class Options(SfDataCommands.Options):
261
+ sobject: str = Field(
262
+ ...,
263
+ description="The API name of the object for the bulk job.",
264
+ )
265
+ file: str = Field(
266
+ ...,
267
+ description="The path to the CSV file that contains the IDs of the records to delete.",
268
+ )
269
+ wait: int = Field(
270
+ None,
271
+ description="The number of minutes to wait for the command to complete.",
272
+ )
273
+
274
+ def _init_task(self):
275
+ super()._init_task()
276
+ self.data_command += "delete bulk"
277
+
278
+ def _init_options(self, kwargs):
279
+ super()._init_options(kwargs)
280
+ if self.parsed_options.sobject:
281
+ self.args.extend(["--sobject", self.parsed_options.sobject])
282
+ if self.parsed_options.file:
283
+ self.args.extend(["--file", self.parsed_options.file])
284
+ if self.parsed_options.wait:
285
+ self.args.extend(["--wait", str(self.parsed_options.wait)])
286
+
287
+ def _run_task(self):
288
+ return super()._run_task()
289
+
290
+
291
+ class DataUpsertBulkTask(SfDataCommands):
292
+ class Options(SfDataCommands.Options):
293
+ sobject: str = Field(
294
+ ...,
295
+ description="The API name of the object for the bulk job.",
296
+ )
297
+ file: str = Field(
298
+ ...,
299
+ description="The path to the CSV file that contains the records to upsert.",
300
+ )
301
+ external_id_field: str = Field(
302
+ ...,
303
+ description="The API name of the external ID field for the upsert.",
304
+ )
305
+ wait: int = Field(
306
+ None,
307
+ description="The number of minutes to wait for the command to complete.",
308
+ )
309
+
310
+ def _init_task(self):
311
+ super()._init_task()
312
+ self.data_command += "upsert bulk"
313
+
314
+ def _init_options(self, kwargs):
315
+ super()._init_options(kwargs)
316
+ if self.parsed_options.sobject:
317
+ self.args.extend(["--sobject", self.parsed_options.sobject])
318
+ if self.parsed_options.file:
319
+ self.args.extend(["--file", self.parsed_options.file])
320
+ if self.parsed_options.external_id_field:
321
+ self.args.extend(
322
+ ["--external-id-field", self.parsed_options.external_id_field]
323
+ )
324
+ if self.parsed_options.wait:
325
+ self.args.extend(["--wait", str(self.parsed_options.wait)])
326
+
327
+ def _run_task(self):
328
+ return super()._run_task()
329
+
330
+
331
+ class DataImportTreeTask(SfDataCommands):
332
+ class Options(SfDataCommands.Options):
333
+ files: list = Field(
334
+ None,
335
+ description="A list of paths to sObject Tree API plan definition files.",
336
+ )
337
+ plan: str = Field(
338
+ None,
339
+ description="The path to a plan definition file.",
340
+ )
341
+ content_type_map: str = Field(
342
+ None,
343
+ description="A mapping of file extensions to content types.",
344
+ )
345
+
346
+ def _init_task(self):
347
+ super()._init_task()
348
+ self.data_command += "import tree"
349
+
350
+ def _init_options(self, kwargs):
351
+ super()._init_options(kwargs)
352
+ if self.parsed_options.files:
353
+ self.args.extend(["--files", ",".join(self.parsed_options.files)])
354
+ if self.parsed_options.plan:
355
+ self.args.extend(["--plan", self.parsed_options.plan])
356
+ if self.parsed_options.content_type_map:
357
+ self.args.extend(
358
+ ["--content-type-map", self.parsed_options.content_type_map]
359
+ )
360
+
361
+ def _run_task(self):
362
+ return super()._run_task()
363
+
364
+
365
+ class DataExportTreeTask(SfDataCommands):
366
+ class Options(SfDataCommands.Options):
367
+ query: str = Field(
368
+ ...,
369
+ description="A SOQL query that retrieves the records you want to export.",
370
+ )
371
+ plan: bool = Field(
372
+ False,
373
+ description="Generate a plan definition file.",
374
+ )
375
+ prefix: str = Field(
376
+ None,
377
+ description="The prefix for the exported data files.",
378
+ )
379
+ output_dir: str = Field(
380
+ None,
381
+ description="The directory to store the exported files.",
382
+ )
383
+
384
+ def _init_task(self):
385
+ super()._init_task()
386
+ self.data_command += "export tree"
387
+
388
+ def _init_options(self, kwargs):
389
+ super()._init_options(kwargs)
390
+ if self.parsed_options.query:
391
+ self.args.extend(["--query", self.parsed_options.query])
392
+ if self.parsed_options.plan:
393
+ self.args.extend(["--plan"])
394
+ if self.parsed_options.prefix:
395
+ self.args.extend(["--prefix", self.parsed_options.prefix])
396
+ if self.parsed_options.output_dir:
397
+ self.args.extend(["--output-dir", self.parsed_options.output_dir])
398
+
399
+ def _run_task(self):
400
+ return super()._run_task()
@@ -23,6 +23,10 @@ class CreateBlankProfile(BaseSalesforceMetadataApiTask):
23
23
  "description": "The description of the the new profile",
24
24
  "required": False,
25
25
  },
26
+ "skip_if_exists": {
27
+ "description": "Skip if the profile already exists in the target org. Defaults to True",
28
+ "required": False,
29
+ },
26
30
  }
27
31
 
28
32
  def _init_options(self, kwargs):
@@ -32,6 +36,7 @@ class CreateBlankProfile(BaseSalesforceMetadataApiTask):
32
36
  "Either the name or the ID of the user license must be set."
33
37
  )
34
38
  self.license = self.options.get("license", "Salesforce")
39
+ self.sf = None
35
40
 
36
41
  def _run_task(self):
37
42
 
@@ -39,6 +44,18 @@ class CreateBlankProfile(BaseSalesforceMetadataApiTask):
39
44
  self.description = self.options.get("description") or ""
40
45
  self.license_id = self.options.get("license_id")
41
46
 
47
+ profile_id = self._get_profile_id(self.name)
48
+ if profile_id:
49
+ if not self.options.get("skip_if_exists", True):
50
+ raise TaskOptionsError(
51
+ f"Profile '{self.name}' already exists with id: {profile_id}"
52
+ )
53
+ self.logger.info(
54
+ f"Profile '{self.name}' already exists with id: {profile_id}"
55
+ )
56
+ self.return_values = {"profile_id": profile_id}
57
+ return profile_id
58
+
42
59
  if not self.license_id:
43
60
  self.license_id = self._get_user_license_id(self.license)
44
61
 
@@ -48,22 +65,38 @@ class CreateBlankProfile(BaseSalesforceMetadataApiTask):
48
65
  self.logger.info(f"Profile '{self.name}' created with id: {result}")
49
66
  return result
50
67
 
68
+ def _get_profile_id(self, profile_name):
69
+ """Returns the Id of a Profile from a given Name"""
70
+ res = self._query_sf(
71
+ f"SELECT Id, Name FROM Profile WHERE FullName = '{profile_name}' LIMIT 1"
72
+ )
73
+ if res["records"]:
74
+ return res["records"][0]["Id"]
75
+
76
+ self.logger.info(f"Profile name '{profile_name}' was not found.")
77
+ return None
78
+
51
79
  def _get_user_license_id(self, license_name):
52
80
  """Returns the Id of a UserLicense from a given Name"""
53
- self.sf = get_simple_salesforce_connection(
54
- self.project_config,
55
- self.org_config,
56
- api_version=self.org_config.latest_api_version,
57
- base_url=None,
58
- )
59
- res = self.sf.query(
81
+ res = self._query_sf(
60
82
  f"SELECT Id, Name FROM UserLicense WHERE Name = '{license_name}' LIMIT 1"
61
83
  )
84
+
62
85
  if res["records"]:
63
86
  return res["records"][0]["Id"]
64
87
  else:
65
88
  raise TaskOptionsError(f"License name '{license_name}' was not found.")
66
89
 
90
+ def _query_sf(self, query):
91
+ self.sf = self.sf or get_simple_salesforce_connection(
92
+ self.project_config,
93
+ self.org_config,
94
+ api_version=self.org_config.latest_api_version,
95
+ base_url=None,
96
+ )
97
+ res = self.sf.query(query)
98
+ return res
99
+
67
100
  def _get_api(self):
68
101
  return self.api_class(
69
102
  self,
@@ -95,6 +95,15 @@ def test_run_task_success():
95
95
  json={
96
96
  "done": True,
97
97
  "totalSize": 1,
98
+ "records": [],
99
+ },
100
+ )
101
+ responses.add(
102
+ responses.GET,
103
+ query_url,
104
+ json={
105
+ "done": True,
106
+ "totalSize": 0,
98
107
  "records": [
99
108
  {
100
109
  "attributes": {
@@ -115,9 +124,12 @@ def test_run_task_success():
115
124
  result = task._run_task()
116
125
  assert result == "001R0000029IyDPIA0"
117
126
  assert responses.calls[0].request.params == {
127
+ "q": "SELECT Id, Name FROM Profile WHERE FullName = 'Test Profile Name' LIMIT 1"
128
+ }
129
+ assert responses.calls[1].request.params == {
118
130
  "q": "SELECT Id, Name FROM UserLicense WHERE Name = 'Foo' LIMIT 1"
119
131
  }
120
- soap_body = responses.calls[1].request.body
132
+ soap_body = responses.calls[2].request.body
121
133
  assert "<Name>Test Profile Name</Name>" in str(soap_body)
122
134
  assert "<UserLicenseId>10056000000VGjUAAW</UserLicenseId>" in str(soap_body)
123
135
  assert "<Description>Have fun stormin da castle</Description>" in str(soap_body)
@@ -135,6 +147,15 @@ def test_run_task_fault():
135
147
  )
136
148
  task.org_config._latest_api_version = "53.0"
137
149
 
150
+ responses.add(
151
+ responses.GET,
152
+ "https://test.salesforce.com/services/data/v53.0/query/",
153
+ json={
154
+ "done": True,
155
+ "totalSize": 0,
156
+ "records": [],
157
+ },
158
+ )
138
159
  responses.add(
139
160
  responses.POST,
140
161
  "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
@@ -157,6 +178,17 @@ def test_run_task_field_error():
157
178
  },
158
179
  )
159
180
  task.org_config._latest_api_version = "53.0"
181
+
182
+ responses.add(
183
+ responses.GET,
184
+ "https://test.salesforce.com/services/data/v53.0/query/",
185
+ json={
186
+ "done": True,
187
+ "totalSize": 0,
188
+ "records": [],
189
+ },
190
+ )
191
+
160
192
  responses.add(
161
193
  responses.POST,
162
194
  "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
@@ -180,6 +212,16 @@ def test_run_task_error():
180
212
  )
181
213
  task.org_config._latest_api_version = "53.0"
182
214
 
215
+ responses.add(
216
+ responses.GET,
217
+ "https://test.salesforce.com/services/data/v53.0/query/",
218
+ json={
219
+ "done": True,
220
+ "totalSize": 0,
221
+ "records": [],
222
+ },
223
+ )
224
+
183
225
  responses.add(
184
226
  responses.POST,
185
227
  "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
@@ -200,3 +242,125 @@ def test_task_options_error():
200
242
  "description": "Foo",
201
243
  },
202
244
  )
245
+
246
+
247
+ @responses.activate
248
+ def test_run_task_success_with_skip_if_exists_false():
249
+ query_url = "https://test.salesforce.com/services/data/v53.0/query/"
250
+
251
+ task = create_task(
252
+ CreateBlankProfile,
253
+ {
254
+ "license": "Foo",
255
+ "name": "Test Profile Name",
256
+ "description": "Have fun stormin da castle",
257
+ },
258
+ )
259
+ task.org_config._latest_api_version = "53.0"
260
+
261
+ responses.add(
262
+ responses.GET,
263
+ query_url,
264
+ json={
265
+ "done": True,
266
+ "totalSize": 1,
267
+ "records": [
268
+ {
269
+ "attributes": {
270
+ "type": "Profile",
271
+ "url": "/services/data/v53.0/sobjects/Profile/10056000000VGjUAAW",
272
+ },
273
+ "Id": "10056000000VGjUAAW",
274
+ "Name": "Test Profile Name",
275
+ }
276
+ ],
277
+ },
278
+ )
279
+
280
+ result = task._run_task()
281
+
282
+ assert result == "10056000000VGjUAAW"
283
+ assert responses.calls[0].request.params == {
284
+ "q": "SELECT Id, Name FROM Profile WHERE FullName = 'Test Profile Name' LIMIT 1"
285
+ }
286
+
287
+
288
+ @responses.activate
289
+ def test_run_task_success_with_skip_if_exists_true():
290
+ query_url = "https://test.salesforce.com/services/data/v53.0/query/"
291
+
292
+ task = create_task(
293
+ CreateBlankProfile,
294
+ {
295
+ "license": "Foo",
296
+ "name": "Test Profile Name",
297
+ "description": "Have fun stormin da castle",
298
+ "skip_if_exists": False,
299
+ },
300
+ )
301
+ task.org_config._latest_api_version = "53.0"
302
+
303
+ responses.add(
304
+ responses.GET,
305
+ query_url,
306
+ json={
307
+ "done": True,
308
+ "totalSize": 1,
309
+ "records": [
310
+ {
311
+ "attributes": {
312
+ "type": "Profile",
313
+ "url": "/services/data/v53.0/sobjects/Profile/10056000000VGjUAAW",
314
+ },
315
+ "Id": "10056000000VGjUAAW",
316
+ "Name": "Test Profile Name",
317
+ }
318
+ ],
319
+ },
320
+ )
321
+
322
+ with pytest.raises(TaskOptionsError) as e:
323
+ task._run_task()
324
+ assert "10056000000VGjUAAW" in str(e)
325
+
326
+
327
+ @responses.activate
328
+ def test_run_task_with_invalid_license():
329
+ query_url = "https://test.salesforce.com/services/data/v53.0/query/"
330
+
331
+ task = create_task(
332
+ CreateBlankProfile,
333
+ {
334
+ "license": "Foo",
335
+ "name": "Test Profile Name",
336
+ "description": "Have fun stormin da castle",
337
+ },
338
+ )
339
+ task.org_config._latest_api_version = "53.0"
340
+
341
+ responses.add(
342
+ responses.GET,
343
+ query_url,
344
+ json={
345
+ "done": True,
346
+ "totalSize": 1,
347
+ "records": [],
348
+ },
349
+ )
350
+ responses.add(
351
+ responses.GET,
352
+ query_url,
353
+ json={
354
+ "done": True,
355
+ "totalSize": 0,
356
+ "records": [],
357
+ },
358
+ )
359
+ responses.add(
360
+ responses.POST,
361
+ "https://test.salesforce.com/services/Soap/u/53.0/ORG_ID",
362
+ RESPONSE_SUCCESS,
363
+ )
364
+ with pytest.raises(TaskOptionsError) as e:
365
+ task._run_task()
366
+ assert "License name 'Foo' was not found." in str(e)
@@ -362,7 +362,7 @@ def test_install_dependency_installs_unmanaged():
362
362
 
363
363
  task._install_dependency(task.dependencies[0])
364
364
  task.dependencies[0].install.assert_called_once_with(
365
- task.project_config, task.org_config
365
+ task.project_config, task.org_config, task.options
366
366
  )
367
367
 
368
368
 
@@ -64,6 +64,9 @@ class UpdateDependencies(BaseSalesforceTask):
64
64
  "base_package_url_format": {
65
65
  "description": "If `interactive` is set to True, display package Ids using a format string ({} will be replaced with the package Id)."
66
66
  },
67
+ "force_pre_post_install": {
68
+ "description": "Forces the pre-install and post-install steps to be run. Defaults to False."
69
+ },
67
70
  **{k: v for k, v in PACKAGE_INSTALL_TASK_OPTIONS.items() if k != "password"},
68
71
  }
69
72
 
@@ -220,8 +223,11 @@ class UpdateDependencies(BaseSalesforceTask):
220
223
  if not click.confirm("Continue to install dependencies?", default=True):
221
224
  raise CumulusCIException("Dependency installation was canceled.")
222
225
 
223
- for d in dependencies:
224
- self._install_dependency(d)
226
+ if dependencies:
227
+ self.logger.info("Installing dependencies:")
228
+
229
+ for d in dependencies:
230
+ self._install_dependency(d)
225
231
 
226
232
  self.org_config.reset_installed_packages()
227
233
 
@@ -233,7 +239,7 @@ class UpdateDependencies(BaseSalesforceTask):
233
239
  self.project_config, self.org_config, self.install_options
234
240
  )
235
241
  else:
236
- dependency.install(self.project_config, self.org_config)
242
+ dependency.install(self.project_config, self.org_config, self.options)
237
243
 
238
244
  def freeze(self, step):
239
245
  if self.options["interactive"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cumulusci-plus
3
- Version: 5.0.18
3
+ Version: 5.0.20
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.18 (2025-08-23)
130
+ ## v5.0.20 (2025-09-05)
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 sf data command of create, query and delete. by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#61](https://github.com/jorgesolebur/CumulusCI/pull/61)
138
+ - Add Skil If Exists for create profile check by [@rupeshjSFDC](https://github.com/rupeshjSFDC) in [#65](https://github.com/jorgesolebur/CumulusCI/pull/65)
139
139
 
140
- **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.17...v5.0.18
140
+ **Full Changelog**: https://github.com/jorgesolebur/CumulusCI/compare/v5.0.19...v5.0.20