cumulusci-plus 5.0.19__py3-none-any.whl → 5.0.35__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.
Files changed (123) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +17 -0
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_service.py +15 -12
  8. cumulusci/cli/tests/test_task.py +88 -2
  9. cumulusci/cli/tests/utils.py +1 -4
  10. cumulusci/core/config/base_task_flow_config.py +26 -1
  11. cumulusci/core/config/project_config.py +2 -20
  12. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  13. cumulusci/core/config/universal_config.py +3 -4
  14. cumulusci/core/dependencies/base.py +1 -1
  15. cumulusci/core/dependencies/dependencies.py +1 -1
  16. cumulusci/core/dependencies/github.py +1 -2
  17. cumulusci/core/dependencies/resolvers.py +1 -1
  18. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  19. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  20. cumulusci/core/flowrunner.py +90 -6
  21. cumulusci/core/github.py +1 -1
  22. cumulusci/core/sfdx.py +3 -1
  23. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  24. cumulusci/core/source_transforms/transforms.py +1 -1
  25. cumulusci/core/tasks.py +13 -2
  26. cumulusci/core/tests/test_flowrunner.py +100 -0
  27. cumulusci/core/tests/test_tasks.py +65 -0
  28. cumulusci/core/utils.py +3 -1
  29. cumulusci/core/versions.py +1 -1
  30. cumulusci/cumulusci.yml +55 -0
  31. cumulusci/oauth/client.py +1 -1
  32. cumulusci/plugins/plugin_base.py +5 -3
  33. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  34. cumulusci/salesforce_api/rest_deploy.py +1 -1
  35. cumulusci/schema/cumulusci.jsonschema.json +64 -0
  36. cumulusci/tasks/apex/anon.py +1 -1
  37. cumulusci/tasks/apex/testrunner.py +416 -142
  38. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  39. cumulusci/tasks/bulkdata/extract.py +0 -1
  40. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  41. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  42. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  43. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  44. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  45. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  46. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  47. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  48. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  49. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  50. cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
  51. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  52. cumulusci/tasks/create_package_version.py +190 -16
  53. cumulusci/tasks/datadictionary.py +1 -1
  54. cumulusci/tasks/metadata_etl/base.py +7 -3
  55. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  56. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  57. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  58. cumulusci/tasks/push/README.md +15 -17
  59. cumulusci/tasks/release_notes/README.md +13 -13
  60. cumulusci/tasks/release_notes/generator.py +13 -8
  61. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  62. cumulusci/tasks/salesforce/Deploy.py +53 -2
  63. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  64. cumulusci/tasks/salesforce/__init__.py +1 -0
  65. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  66. cumulusci/tasks/salesforce/composite.py +1 -1
  67. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  68. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  69. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  70. cumulusci/tasks/salesforce/profiles.py +13 -9
  71. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  72. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  73. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  74. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  75. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  76. cumulusci/tasks/salesforce/tests/test_profiles.py +43 -3
  77. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  78. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  79. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  80. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  81. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  82. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  83. cumulusci/tasks/salesforce/update_profile.py +17 -13
  84. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  85. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  86. cumulusci/tasks/sfdmu/__init__.py +0 -0
  87. cumulusci/tasks/sfdmu/sfdmu.py +363 -0
  88. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  89. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  90. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  91. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  92. cumulusci/tasks/tests/test_util.py +42 -0
  93. cumulusci/tasks/util.py +37 -1
  94. cumulusci/tasks/utility/copyContents.py +402 -0
  95. cumulusci/tasks/utility/credentialManager.py +256 -0
  96. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  97. cumulusci/tasks/utility/env_management.py +1 -1
  98. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  99. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  100. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  101. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  102. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  103. cumulusci/tests/test_integration_infrastructure.py +3 -1
  104. cumulusci/tests/test_utils.py +70 -6
  105. cumulusci/utils/__init__.py +54 -9
  106. cumulusci/utils/classutils.py +5 -2
  107. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  108. cumulusci/utils/options.py +23 -1
  109. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  110. cumulusci/utils/yaml/cumulusci_yml.py +7 -3
  111. cumulusci/utils/yaml/model_parser.py +2 -2
  112. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  113. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  114. cumulusci/vcs/base.py +23 -15
  115. cumulusci/vcs/bootstrap.py +5 -4
  116. cumulusci/vcs/utils/list_modified_files.py +189 -0
  117. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  118. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
  119. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +123 -98
  120. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
  121. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
  122. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
  123. {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
@@ -140,6 +140,10 @@ class Snowfakery(BaseSalesforceApiTask):
140
140
  "description": "Boolean: should we continue loading even after running into row errors? "
141
141
  "Defaults to False."
142
142
  },
143
+ "validate_only": {
144
+ "description": "Boolean: if True, only validate the generated mapping against the org schema without loading data. "
145
+ "Defaults to False."
146
+ },
143
147
  }
144
148
 
145
149
  def _validate_options(self):
@@ -160,6 +164,7 @@ class Snowfakery(BaseSalesforceApiTask):
160
164
  self.drop_missing_schema = process_bool_arg(
161
165
  self.options.get("drop_missing_schema", False)
162
166
  )
167
+ self.validate_only = process_bool_arg(self.options.get("validate_only", False))
163
168
 
164
169
  loading_rules = process_list_arg(self.options.get("loading_rules")) or []
165
170
  self.loading_rules = [Path(path) for path in loading_rules if path]
@@ -230,14 +235,19 @@ class Snowfakery(BaseSalesforceApiTask):
230
235
  def _run_task(self):
231
236
  self.setup()
232
237
 
233
- portions = PortionGenerator(
234
- self.run_until.gap,
235
- MIN_PORTION_SIZE,
236
- MAX_PORTION_SIZE,
237
- )
238
-
239
238
  working_directory = self.options.get("working_directory")
240
239
  with self.workingdir_or_tempdir(working_directory) as working_directory:
240
+ # Route to validation flow if validate_only is True
241
+ if self.validate_only:
242
+ return self._run_validation(working_directory)
243
+
244
+ # Normal data generation and loading flow
245
+ portions = PortionGenerator(
246
+ self.run_until.gap,
247
+ MIN_PORTION_SIZE,
248
+ MAX_PORTION_SIZE,
249
+ )
250
+
241
251
  self._setup_channels_and_queues(working_directory)
242
252
  self.logger.info(f"Working directory is {working_directory}")
243
253
 
@@ -544,32 +554,20 @@ class Snowfakery(BaseSalesforceApiTask):
544
554
 
545
555
  template_dir = Path(working_directory) / "template_1"
546
556
  template_dir.mkdir()
547
- # changes here should often be reflected in
548
- # data_generator_opts and data_loader_opts
549
557
 
550
558
  channel_decl = self.channel_configs[0]
551
559
 
552
- plugin_options = {
553
- "pid": "0",
554
- "big_ids": "True",
555
- }
556
560
  # if it's efficient to do the whole load in one go, let's just do that.
557
561
  if self.run_until.gap < MIN_PORTION_SIZE:
558
562
  num_records = self.run_until.gap
559
563
  else:
560
564
  num_records = 1 # smallest possible batch to get to parallelizing fast
565
+
566
+ batch_options = self._prepare_initial_batch_options(num_records)
561
567
  results = self._generate_and_load_batch(
562
568
  template_dir,
563
569
  channel_decl.org_config,
564
- {
565
- "generator_yaml": self.options.get("recipe"),
566
- "num_records": num_records,
567
- "num_records_tablename": self.run_until.sobject_name or COUNT_REPS,
568
- "loading_rules": self.loading_rules,
569
- "vars": channel_decl.merge_recipe_options(self.recipe_options),
570
- "plugin_options": plugin_options,
571
- "bulk_mode": self.bulk_mode,
572
- },
570
+ batch_options,
573
571
  )
574
572
  self.update_running_totals_from_load_step_results(results)
575
573
 
@@ -595,9 +593,19 @@ class Snowfakery(BaseSalesforceApiTask):
595
593
 
596
594
  return template_dir, wd.relevant_sobjects()
597
595
 
598
- def _generate_and_load_batch(self, tempdir, org_config, options) -> dict:
599
- """Before the "full" dataload starts we do a single batch to
600
- load singletons.
596
+ def _run_generate_and_load_subtask(
597
+ self, tempdir, org_config, options, validate_only=False
598
+ ):
599
+ """Run GenerateAndLoadDataFromYaml subtask with given options.
600
+
601
+ Args:
602
+ tempdir: Working directory for generated files
603
+ org_config: Org configuration
604
+ options: Options dict for the subtask
605
+ validate_only: If True, only validate mapping without loading
606
+
607
+ Returns:
608
+ dict: Subtask return values
601
609
  """
602
610
  options = {
603
611
  **options,
@@ -605,6 +613,7 @@ class Snowfakery(BaseSalesforceApiTask):
605
613
  "set_recently_viewed": False,
606
614
  "ignore_row_errors": self.ignore_row_errors,
607
615
  "drop_missing_schema": self.drop_missing_schema,
616
+ "validate_only": validate_only,
608
617
  }
609
618
  subtask_config = TaskConfig({"options": options})
610
619
  subtask = GenerateAndLoadDataFromYaml(
@@ -616,7 +625,73 @@ class Snowfakery(BaseSalesforceApiTask):
616
625
  stepnum=self.stepnum,
617
626
  )
618
627
  subtask()
619
- return subtask.return_values["load_results"][0]
628
+ return subtask.return_values
629
+
630
+ def _prepare_initial_batch_options(self, num_records: int) -> dict:
631
+ """Prepare options for initial data generation batch.
632
+
633
+ Args:
634
+ num_records: Number of records to generate
635
+
636
+ Returns:
637
+ dict: Options for GenerateAndLoadDataFromYaml subtask
638
+ """
639
+ channel_decl = self.channel_configs[0]
640
+
641
+ plugin_options = {
642
+ "pid": "0",
643
+ "big_ids": "True",
644
+ }
645
+
646
+ return {
647
+ "generator_yaml": self.options.get("recipe"),
648
+ "num_records": num_records,
649
+ "num_records_tablename": self.run_until.sobject_name or COUNT_REPS,
650
+ "loading_rules": self.loading_rules,
651
+ "vars": channel_decl.merge_recipe_options(self.recipe_options),
652
+ "plugin_options": plugin_options,
653
+ "bulk_mode": self.bulk_mode,
654
+ }
655
+
656
+ def _run_validation(self, working_directory: Path):
657
+ """Run validation flow: generate minimal data and validate mapping.
658
+
659
+ Args:
660
+ working_directory: Working directory for generated files
661
+
662
+ Returns:
663
+ dict: return_values with validation_result
664
+ """
665
+ template_dir = Path(working_directory) / "template_validation"
666
+ template_dir.mkdir()
667
+
668
+ channel_decl = self.channel_configs[0]
669
+
670
+ # Prepare options for validation
671
+ batch_options = self._prepare_initial_batch_options(num_records=1)
672
+
673
+ # Run generation and validation
674
+ subtask_return_values = self._run_generate_and_load_subtask(
675
+ template_dir,
676
+ channel_decl.org_config,
677
+ batch_options,
678
+ validate_only=True,
679
+ )
680
+
681
+ # Set return values with validation result
682
+ self.return_values = {
683
+ "validation_result": subtask_return_values["validation_result"]
684
+ }
685
+ return self.return_values
686
+
687
+ def _generate_and_load_batch(self, tempdir, org_config, options) -> dict:
688
+ """Before the "full" dataload starts we do a single batch to
689
+ load singletons.
690
+ """
691
+ subtask_return_values = self._run_generate_and_load_subtask(
692
+ tempdir, org_config, options, validate_only=False
693
+ )
694
+ return subtask_return_values["load_results"][0]
620
695
 
621
696
  def _cleanup_object_tables(self, engine, metadata):
622
697
  """Delete all tables that do not relate to id->OID mapping"""
@@ -250,3 +250,162 @@ class TestGenerateAndLoadData:
250
250
  )
251
251
  task()
252
252
  assert list(Path(t).glob("*"))
253
+
254
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._dataload")
255
+ @mock.patch(
256
+ "cumulusci.tasks.bulkdata.generate_and_load_data.validate_and_inject_mapping"
257
+ )
258
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._datagen")
259
+ def test_validate_only_mode(self, mock_datagen, mock_validate, _dataload):
260
+ """Test that validate_only mode validates without loading data"""
261
+ from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
262
+
263
+ mapping_file = os.path.join(os.path.dirname(__file__), "mapping_vanilla_sf.yml")
264
+
265
+ # Mock ValidationResult
266
+ validation_result = ValidationResult()
267
+ mock_validate.return_value = validation_result
268
+
269
+ task = _make_task(
270
+ GenerateAndLoadData,
271
+ {
272
+ "options": {
273
+ "num_records": 12,
274
+ "mapping": mapping_file,
275
+ "data_generation_task": "cumulusci.tasks.bulkdata.tests.dummy_data_factory.GenerateDummyData",
276
+ "validate_only": True,
277
+ }
278
+ },
279
+ )
280
+
281
+ task()
282
+
283
+ # Verify data generation was called (to create mapping)
284
+ mock_datagen.assert_called_once()
285
+
286
+ # Verify validation was called
287
+ mock_validate.assert_called_once()
288
+
289
+ # Verify load was NOT called
290
+ _dataload.assert_not_called()
291
+
292
+ # Verify return values contain validation_result
293
+ assert "validation_result" in task.return_values
294
+ assert task.return_values["validation_result"] == validation_result
295
+
296
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._dataload")
297
+ @mock.patch(
298
+ "cumulusci.tasks.bulkdata.generate_and_load_data.validate_and_inject_mapping"
299
+ )
300
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._datagen")
301
+ def test_validate_only_with_errors(self, mock_datagen, mock_validate, _dataload):
302
+ """Test that validate_only mode returns errors without raising exception"""
303
+ from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
304
+
305
+ mapping_file = os.path.join(os.path.dirname(__file__), "mapping_vanilla_sf.yml")
306
+
307
+ # Mock ValidationResult with errors
308
+ validation_result = ValidationResult()
309
+ validation_result.add_error("Test error: Field does not exist")
310
+ validation_result.add_warning("Test warning: Field has no permissions")
311
+ mock_validate.return_value = validation_result
312
+
313
+ task = _make_task(
314
+ GenerateAndLoadData,
315
+ {
316
+ "options": {
317
+ "num_records": 12,
318
+ "mapping": mapping_file,
319
+ "data_generation_task": "cumulusci.tasks.bulkdata.tests.dummy_data_factory.GenerateDummyData",
320
+ "validate_only": True,
321
+ }
322
+ },
323
+ )
324
+
325
+ # Should not raise exception even with errors
326
+ task()
327
+
328
+ # Verify data generation was called
329
+ mock_datagen.assert_called_once()
330
+
331
+ # Verify validation was called
332
+ mock_validate.assert_called_once()
333
+
334
+ # Verify load was NOT called
335
+ _dataload.assert_not_called()
336
+
337
+ # Verify return values contain validation_result with errors
338
+ assert "validation_result" in task.return_values
339
+ assert task.return_values["validation_result"].has_errors()
340
+ assert len(task.return_values["validation_result"].errors) == 1
341
+ assert len(task.return_values["validation_result"].warnings) == 1
342
+
343
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._dataload")
344
+ def test_validate_only_false_loads_data(self, _dataload):
345
+ """Test that validate_only=False performs normal data loading"""
346
+ mapping_file = os.path.join(os.path.dirname(__file__), "mapping_vanilla_sf.yml")
347
+
348
+ task = _make_task(
349
+ GenerateAndLoadData,
350
+ {
351
+ "options": {
352
+ "num_records": 12,
353
+ "mapping": mapping_file,
354
+ "data_generation_task": "cumulusci.tasks.bulkdata.tests.dummy_data_factory.GenerateDummyData",
355
+ "validate_only": False,
356
+ }
357
+ },
358
+ )
359
+
360
+ task()
361
+
362
+ # Verify load WAS called
363
+ _dataload.assert_called_once()
364
+
365
+ # Verify return values contain load_results, not validation_result
366
+ assert "load_results" in task.return_values
367
+ assert "validation_result" not in task.return_values
368
+
369
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._dataload")
370
+ @mock.patch(
371
+ "cumulusci.tasks.bulkdata.generate_and_load_data.validate_and_inject_mapping"
372
+ )
373
+ @mock.patch("cumulusci.tasks.bulkdata.GenerateAndLoadData._datagen")
374
+ def test_validate_only_with_working_directory(
375
+ self, mock_datagen, mock_validate, _dataload
376
+ ):
377
+ """Test that validate_only respects working_directory option"""
378
+ from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
379
+
380
+ mapping_file = os.path.join(os.path.dirname(__file__), "mapping_vanilla_sf.yml")
381
+
382
+ validation_result = ValidationResult()
383
+ mock_validate.return_value = validation_result
384
+
385
+ with TemporaryDirectory() as t:
386
+ task = _make_task(
387
+ GenerateAndLoadData,
388
+ {
389
+ "options": {
390
+ "num_records": 12,
391
+ "mapping": mapping_file,
392
+ "data_generation_task": "cumulusci.tasks.bulkdata.tests.dummy_data_factory.GenerateDummyData",
393
+ "validate_only": True,
394
+ "working_directory": t,
395
+ }
396
+ },
397
+ )
398
+
399
+ task()
400
+
401
+ # Verify data generation was called
402
+ mock_datagen.assert_called_once()
403
+
404
+ # Verify validation was called
405
+ mock_validate.assert_called_once()
406
+
407
+ # Verify load was NOT called
408
+ _dataload.assert_not_called()
409
+
410
+ # Verify working directory was used (should have generated files)
411
+ assert list(Path(t).glob("*"))
@@ -2715,8 +2715,6 @@ class TestLoadData:
2715
2715
  chunks_index = 0
2716
2716
 
2717
2717
  def fetchmany(batch_size):
2718
- nonlocal chunks_index
2719
-
2720
2718
  assert 200 == batch_size
2721
2719
 
2722
2720
  # _generate_contact_id_map_for_person_accounts should break if fetchmany returns falsy.