cumulusci-plus 5.0.21__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 (121) 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/sourcetracking.py +1 -1
  71. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  72. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  73. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  74. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  75. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  76. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  77. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  78. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  79. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  80. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  81. cumulusci/tasks/salesforce/update_profile.py +17 -13
  82. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  83. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  84. cumulusci/tasks/sfdmu/__init__.py +0 -0
  85. cumulusci/tasks/sfdmu/sfdmu.py +363 -0
  86. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  87. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  88. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  89. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  90. cumulusci/tasks/tests/test_util.py +42 -0
  91. cumulusci/tasks/util.py +37 -1
  92. cumulusci/tasks/utility/copyContents.py +402 -0
  93. cumulusci/tasks/utility/credentialManager.py +256 -0
  94. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  95. cumulusci/tasks/utility/env_management.py +1 -1
  96. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  97. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  98. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  99. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  100. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  101. cumulusci/tests/test_integration_infrastructure.py +3 -1
  102. cumulusci/tests/test_utils.py +70 -6
  103. cumulusci/utils/__init__.py +54 -9
  104. cumulusci/utils/classutils.py +5 -2
  105. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  106. cumulusci/utils/options.py +23 -1
  107. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  108. cumulusci/utils/yaml/cumulusci_yml.py +7 -3
  109. cumulusci/utils/yaml/model_parser.py +2 -2
  110. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  111. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  112. cumulusci/vcs/base.py +23 -15
  113. cumulusci/vcs/bootstrap.py +5 -4
  114. cumulusci/vcs/utils/list_modified_files.py +189 -0
  115. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  116. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
  117. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +121 -96
  118. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
  119. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
  120. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
  121. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
@@ -35,6 +35,7 @@ def capture_orgid_using_task(create_task: callable, tmp_path: str) -> str:
35
35
 
36
36
  class TestIntegrationInfrastructure:
37
37
  "Test our two plugins for doing integration testing"
38
+
38
39
  remembered_cli_specified_org_id = None
39
40
 
40
41
  @pytest.mark.vcr()
@@ -42,6 +43,7 @@ class TestIntegrationInfrastructure:
42
43
  self, create_task, run_code_without_recording
43
44
  ):
44
45
  "Delete a VCR Cassette to ensure next test creates it."
46
+
45
47
  # only delete the cassette if we can replace it
46
48
  def delete_cassette():
47
49
  if first_cassette.exists():
@@ -81,7 +83,7 @@ class TestIntegrationInfrastructure:
81
83
 
82
84
  run_code_without_recording(setup)
83
85
 
84
- def test_file_was_not_created(self, capture_orgid_using_task):
86
+ def test_file_was_not_created(self):
85
87
  filename = (
86
88
  Path(__file__).parent
87
89
  / "cassettes/TestIntegrationInfrastructure.test_run_code_without_recording.yaml"
@@ -453,31 +453,95 @@ Options\n------------------------------------------\n\n
453
453
  def test_inject_namespace__managed(self):
454
454
  logger = mock.Mock()
455
455
  name = "___NAMESPACE___test"
456
- content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%"
456
+ content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%|%%%NAMESPACE_COLON%%%|%%%NAMESPACED_ORG_COLON%%%"
457
457
 
458
458
  name, content = utils.inject_namespace(
459
459
  name, content, namespace="ns", managed=True, logger=logger
460
460
  )
461
461
  assert name == "ns__test"
462
- assert content == "ns__|ns.||ns|c"
462
+ assert content == "ns__|ns.||ns|c|ns:|"
463
463
 
464
464
  def test_inject_namespace__unmanaged(self):
465
465
  name = "___NAMESPACE___test"
466
- content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%"
466
+ content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%|%%%NAMESPACE_COLON%%%|%%%NAMESPACED_ORG_COLON%%%"
467
467
 
468
468
  name, content = utils.inject_namespace(name, content, namespace="ns")
469
469
  assert name == "test"
470
- assert content == "|||c|c"
470
+ assert content == "|||c|c||"
471
471
 
472
472
  def test_inject_namespace__namespaced_org(self):
473
473
  name = "___NAMESPACE___test"
474
- content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%"
474
+ content = "%%%NAMESPACE%%%|%%%NAMESPACE_DOT%%%|%%%NAMESPACED_ORG%%%|%%%NAMESPACE_OR_C%%%|%%%NAMESPACED_ORG_OR_C%%%|%%%NAMESPACE_COLON%%%|%%%NAMESPACED_ORG_COLON%%%"
475
475
 
476
476
  name, content = utils.inject_namespace(
477
477
  name, content, namespace="ns", managed=True, namespaced_org=True
478
478
  )
479
479
  assert name == "ns__test"
480
- assert content == "ns__|ns.|ns__|ns|ns"
480
+ assert content == "ns__|ns.|ns__|ns|ns|ns:|ns:"
481
+
482
+ def test_inject_namespace__namespace_colon_managed(self):
483
+ """Test %%%NAMESPACE_COLON%%% token with managed=True"""
484
+ logger = mock.Mock()
485
+ name = "test"
486
+ content = "%%%NAMESPACE_COLON%%%component"
487
+
488
+ name, content = utils.inject_namespace(
489
+ name, content, namespace="ns", managed=True, logger=logger
490
+ )
491
+ assert content == "ns:component"
492
+ logger.info.assert_called()
493
+
494
+ def test_inject_namespace__namespace_colon_unmanaged(self):
495
+ """Test %%%NAMESPACE_COLON%%% token with managed=False"""
496
+ name = "test"
497
+ content = "%%%NAMESPACE_COLON%%%component"
498
+
499
+ name, content = utils.inject_namespace(
500
+ name, content, namespace="ns", managed=False
501
+ )
502
+ assert content == "component"
503
+
504
+ def test_inject_namespace__namespace_colon_no_namespace(self):
505
+ """Test %%%NAMESPACE_COLON%%% token with no namespace"""
506
+ name = "test"
507
+ content = "%%%NAMESPACE_COLON%%%component"
508
+
509
+ name, content = utils.inject_namespace(
510
+ name, content, namespace=None, managed=True
511
+ )
512
+ assert content == "component"
513
+
514
+ def test_inject_namespace__namespaced_org_colon(self):
515
+ """Test %%%NAMESPACED_ORG_COLON%%% token with namespaced_org=True"""
516
+ logger = mock.Mock()
517
+ name = "test"
518
+ content = "%%%NAMESPACED_ORG_COLON%%%component"
519
+
520
+ name, content = utils.inject_namespace(
521
+ name, content, namespace="ns", namespaced_org=True, logger=logger
522
+ )
523
+ assert content == "ns:component"
524
+ logger.info.assert_called()
525
+
526
+ def test_inject_namespace__namespaced_org_colon_false(self):
527
+ """Test %%%NAMESPACED_ORG_COLON%%% token with namespaced_org=False"""
528
+ name = "test"
529
+ content = "%%%NAMESPACED_ORG_COLON%%%component"
530
+
531
+ name, content = utils.inject_namespace(
532
+ name, content, namespace="ns", namespaced_org=False
533
+ )
534
+ assert content == "component"
535
+
536
+ def test_inject_namespace__namespaced_org_colon_no_namespace(self):
537
+ """Test %%%NAMESPACED_ORG_COLON%%% token with no namespace"""
538
+ name = "test"
539
+ content = "%%%NAMESPACED_ORG_COLON%%%component"
540
+
541
+ name, content = utils.inject_namespace(
542
+ name, content, namespace=None, namespaced_org=True
543
+ )
544
+ assert content == "component"
481
545
 
482
546
  def test_inject_namespace__filename_token_in_package_xml(self):
483
547
  name, content = utils.inject_namespace(
@@ -42,7 +42,7 @@ BREW_DEPRECATION_MSG = (
42
42
  "brew uninstall cumulusci-plus\nbrew install pipx\npipx ensurepath\npipx install cumulusci-plus"
43
43
  )
44
44
  PIP_UPDATE_CMD = "pip install --upgrade cumulusci-plus"
45
- PIPX_UPDATE_CMD = "pipx upgrade cumulusci-plus"
45
+ PIPX_UPDATE_CMD = "pipx install cumulusci-plus-azure-devops --include-deps --force"
46
46
 
47
47
 
48
48
  def parse_api_datetime(value):
@@ -245,6 +245,10 @@ def inject_namespace(
245
245
  ):
246
246
  """Replaces %%%NAMESPACE%%% in file content and ___NAMESPACE___ in file name
247
247
  with either '' if no namespace is provided or 'namespace__' if provided.
248
+
249
+ Also handles:
250
+ - %%%MANAGED_OR_NAMESPACED_ORG%%% and ___MANAGED_OR_NAMESPACED_ORG___ tokens
251
+ which are replaced with 'namespace__' if managed=True OR namespaced_org=True
248
252
  """
249
253
 
250
254
  # Handle namespace and filename tokens
@@ -264,7 +268,15 @@ def inject_namespace(
264
268
  # Handle tokens %%%NAMESPACED_ORG%%% and ___NAMESPACED_ORG___
265
269
  namespaced_org_token = "%%%NAMESPACED_ORG%%%"
266
270
  namespaced_org_file_token = "___NAMESPACED_ORG___"
267
- namespaced_org = namespace + "__" if namespaced_org else ""
271
+ namespaced_org = (namespace + "__") if namespaced_org and namespace else ""
272
+
273
+ # Handle tokens %%%NAMESPACED_ORG_COLON%%%
274
+ namespaced_org_colon_token = "%%%NAMESPACED_ORG_COLON%%%"
275
+ namespaced_org_colon = (namespace + ":") if namespaced_org and namespace else ""
276
+
277
+ # Handle tokens %%%NAMESPACE_COLON%%%
278
+ namespace_colon_token = "%%%NAMESPACE_COLON%%%"
279
+ namespace_colon = (namespace + ":") if managed and namespace else ""
268
280
 
269
281
  # Handle token %%%NAMESPACE_OR_C%%% for lightning components
270
282
  namespace_or_c_token = "%%%NAMESPACE_OR_C%%%"
@@ -274,6 +286,13 @@ def inject_namespace(
274
286
  namespaced_org_or_c_token = "%%%NAMESPACED_ORG_OR_C%%%"
275
287
  namespaced_org_or_c = namespace if namespaced_org else "c"
276
288
 
289
+ # Handle new tokens %%%MANAGED_OR_NAMESPACED_ORG%%% and ___MANAGED_OR_NAMESPACED_ORG___
290
+ managed_or_namespaced_org_token = "%%%MANAGED_OR_NAMESPACED_ORG%%%"
291
+ managed_or_namespaced_org_file_token = "___MANAGED_OR_NAMESPACED_ORG___"
292
+ managed_or_namespaced_org = (
293
+ (namespace + "__") if ((managed) or (namespaced_org)) and namespace else ""
294
+ )
295
+
277
296
  orig_name = name
278
297
  prev_content = content
279
298
  content = content.replace(namespace_token, namespace_prefix)
@@ -317,6 +336,20 @@ def inject_namespace(
317
336
  f' {name}: Replaced {namespaced_org_token} with "{namespaced_org}"'
318
337
  )
319
338
 
339
+ prev_content = content
340
+ content = content.replace(namespaced_org_colon_token, namespaced_org_colon)
341
+ if logger and content != prev_content:
342
+ logger.info(
343
+ f' {name}: Replaced {namespaced_org_colon_token} with "{namespaced_org_colon}"'
344
+ )
345
+
346
+ prev_content = content
347
+ content = content.replace(namespace_colon_token, namespace_colon)
348
+ if logger and content != prev_content:
349
+ logger.info(
350
+ f' {name}: Replaced {namespace_colon_token} with "{namespace_colon}"'
351
+ )
352
+
320
353
  prev_content = content
321
354
  content = content.replace(namespaced_org_or_c_token, namespaced_org_or_c)
322
355
  if logger and content != prev_content:
@@ -324,9 +357,20 @@ def inject_namespace(
324
357
  f' {name}: Replaced {namespaced_org_or_c_token} with "{namespaced_org_or_c}"'
325
358
  )
326
359
 
360
+ # Replace new managed_or_namespaced_org token in content
361
+ prev_content = content
362
+ content = content.replace(
363
+ managed_or_namespaced_org_token, managed_or_namespaced_org
364
+ )
365
+ if logger and content != prev_content:
366
+ logger.info(
367
+ f' {name}: Replaced {managed_or_namespaced_org_token} with "{managed_or_namespaced_org}"'
368
+ )
369
+
327
370
  # Replace namespace token in file name
328
371
  name = name.replace(filename_token, namespace_prefix)
329
372
  name = name.replace(namespaced_org_file_token, namespaced_org)
373
+ name = name.replace(managed_or_namespaced_org_file_token, managed_or_namespaced_org)
330
374
  if logger and name != orig_name:
331
375
  logger.info(f" {orig_name}: renamed to {name}")
332
376
 
@@ -737,11 +781,12 @@ def get_tasks_with_options(project_config, frozen_options: frozenset):
737
781
  coordinator_opts = {}
738
782
  options = dict(frozen_options)
739
783
  for task_config in project_config.list_tasks():
740
- if any(
741
- key in options.keys()
742
- for key in project_config.get_task(task_config["name"])
743
- .get_class()
744
- .task_options.keys()
745
- ):
746
- coordinator_opts[task_config["name"]] = options
784
+ task_keys = (
785
+ project_config.get_task(task_config["name"]).get_class().task_options.keys()
786
+ )
787
+ if any(key in options.keys() for key in task_keys):
788
+ # only assign options that are actually used by the task. This is to avoid passing options that are not used by the task.
789
+ coordinator_opts[task_config["name"]] = {
790
+ key: options[key] for key in options.keys() if key in task_keys
791
+ }
747
792
  return coordinator_opts
@@ -1,9 +1,12 @@
1
- def get_all_subclasses(cls):
1
+ from typing import Any, Dict, Set, Type
2
+
3
+
4
+ def get_all_subclasses(cls: Type[Any]) -> Set[Type[Any]]:
2
5
  """Return all subclasses of the given class"""
3
6
  return set(cls.__subclasses__()).union(
4
7
  [s for c in cls.__subclasses__() for s in get_all_subclasses(c)]
5
8
  )
6
9
 
7
10
 
8
- def namedtuple_as_simple_dict(self):
11
+ def namedtuple_as_simple_dict(self: Any) -> Dict[str, Any]:
9
12
  return {name: getattr(self, name, None) for name in self.__class__._fields}
@@ -1,32 +1,33 @@
1
1
  interactions:
2
- - request:
3
- body: '{"compositeRequest": [{"referenceId": "CCI__RefId__0__", "method": "GET",
4
- "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
5
- "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__1__", "method":
6
- "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
7
- "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__2__", "method":
8
- "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
9
- "Thu, 03 Sep 2020 21:35:07 GMT"}}]}'
10
- headers:
11
- Request-Headers:
12
- - Elided
13
- method: POST
14
- uri: https://orgname.my.salesforce.com/services/data/v49.0/composite
15
- response:
16
- body:
17
- string: "{\n \"compositeResponse\" : [ {\n \"body\" : null,\n \"httpHeaders\"
18
- : {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
19
- 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
20
- : \"CCI__RefId__0__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" :
21
- {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
22
- 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
23
- : \"CCI__RefId__1__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" :
24
- {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
25
- 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
26
- : \"CCI__RefId__2__\"\n } ]\n}"
27
- headers:
28
- Response-Headers: Elided
29
- status:
30
- code: 200
31
- message: OK
2
+ - request:
3
+ body: '{"compositeRequest": [{"referenceId": "CCI__RefId__0__", "method": "GET",
4
+ "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
5
+ "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__1__", "method":
6
+ "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
7
+ "Thu, 03 Sep 2020 21:35:07 GMT"}}, {"referenceId": "CCI__RefId__2__", "method":
8
+ "GET", "url": "/services/data/v49.0/sobjects", "httpHeaders": {"If-Modified-Since":
9
+ "Thu, 03 Sep 2020 21:35:07 GMT"}}]}'
10
+ headers:
11
+ Request-Headers:
12
+ - Elided
13
+ method: POST
14
+ uri: https://orgname.my.salesforce.com/services/data/v49.0/composite
15
+ response:
16
+ body:
17
+ string:
18
+ "{\n \"compositeResponse\" : [ {\n \"body\" : null,\n \"httpHeaders\"
19
+ : {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
20
+ 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
21
+ : \"CCI__RefId__0__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" :
22
+ {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
23
+ 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
24
+ : \"CCI__RefId__1__\"\n }, {\n \"body\" : null,\n \"httpHeaders\" :
25
+ {\n \"ETag\" : \"\\\"baf7f695\\\"\",\n \"Last-Modified\" : \"Fri,
26
+ 14 Aug 2020 20:53:02 GMT\"\n },\n \"httpStatusCode\" : 304,\n \"referenceId\"
27
+ : \"CCI__RefId__2__\"\n } ]\n}"
28
+ headers:
29
+ Response-Headers: Elided
30
+ status:
31
+ code: 200
32
+ message: OK
32
33
  version: 1
@@ -2,7 +2,7 @@ import json
2
2
  from inspect import signature
3
3
  from typing import Any, Dict, List
4
4
 
5
- from pydantic import DirectoryPath, Field, FilePath, create_model
5
+ from pydantic.v1 import DirectoryPath, Field, FilePath, create_model
6
6
 
7
7
  from cumulusci.core.exceptions import TaskOptionsError
8
8
  from cumulusci.utils.yaml.model_parser import CCIDictModel
@@ -95,6 +95,8 @@ class ListOfStringsOption(CCIOptionType):
95
95
 
96
96
  @classmethod
97
97
  def from_str(cls, v) -> List[str]:
98
+ if v is None or v == "":
99
+ return []
98
100
  return [s.strip() for s in v.split(",")]
99
101
 
100
102
 
@@ -106,6 +108,26 @@ class MappingOption(CCIOptionType):
106
108
  return parse_list_of_pairs_dict_arg(v)
107
109
 
108
110
 
111
+ class PercentageOption(CCIOptionType):
112
+ """Parses a percentage from a string in format X%"""
113
+
114
+ @classmethod
115
+ def validate(cls, v):
116
+ """Validate and convert a value.
117
+ If its a string, parse it, else, just validate it.
118
+ """
119
+ try:
120
+ return v if isinstance(v, int) else int(v.rstrip("%"))
121
+ except ValueError:
122
+ raise TaskOptionsError(
123
+ "Value should be a percentage or integer (e.g. 90% or 90)"
124
+ )
125
+
126
+ @classmethod
127
+ def from_str(cls, v) -> int:
128
+ return v
129
+
130
+
109
131
  def parse_list_of_pairs_dict_arg(arg):
110
132
  """Process an arg in the format "aa:bb,cc:dd" """
111
133
  if isinstance(arg, dict):
@@ -7,7 +7,7 @@ from multiprocessing import Queue
7
7
  from pathlib import Path
8
8
  from traceback import format_exc
9
9
 
10
- from pydantic import BaseModel
10
+ from pydantic.v1 import BaseModel
11
11
 
12
12
  from cumulusci.core.config import (
13
13
  BaseConfig,
@@ -9,8 +9,8 @@ from logging import getLogger
9
9
  from pathlib import Path
10
10
  from typing import Any, Dict, List, Optional, Sequence, Union
11
11
 
12
- from pydantic import Field, root_validator, validator
13
- from pydantic.types import DirectoryPath
12
+ from pydantic.v1 import Field, root_validator, validator
13
+ from pydantic.v1.types import DirectoryPath
14
14
  from typing_extensions import Literal, TypedDict
15
15
 
16
16
  from cumulusci.core.enums import StrEnum
@@ -84,6 +84,11 @@ class Package(CCIDictModel):
84
84
  uninstall_class: str = None
85
85
  api_version: str = None
86
86
  metadata_package_id: str = None
87
+ apex_test_access: Optional[dict[str, Union[str, list[str]]]] = None
88
+ package_metadata_access: Optional[dict[str, Union[str, list[str]]]] = None
89
+ unpackaged_metadata_path: Optional[
90
+ Union[str, List[str], Dict[str, Union[str, List[str]]]]
91
+ ] = None
87
92
 
88
93
 
89
94
  class Test(CCIDictModel):
@@ -321,7 +326,6 @@ class ErrorDict(TypedDict):
321
326
 
322
327
  def _log_yaml_errors(logger, errors: List[ErrorDict]):
323
328
  "Format and log a Pydantic-style error dictionary"
324
- # global has_shown_yaml_error_message
325
329
  plural = "" if len(errors) <= 1 else "s"
326
330
  logger.warning(f"CumulusCI Configuration Warning{plural}:")
327
331
  for error in errors:
@@ -1,8 +1,8 @@
1
1
  from pathlib import Path
2
2
  from typing import IO, Sequence, Union
3
3
 
4
- from pydantic import BaseModel, ValidationError
5
- from pydantic.error_wrappers import ErrorWrapper
4
+ from pydantic.v1 import BaseModel, ValidationError
5
+ from pydantic.v1.error_wrappers import ErrorWrapper
6
6
 
7
7
  from cumulusci.utils.yaml.safer_loader import load_from_source, load_yaml_data
8
8
 
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from unittest.mock import MagicMock, Mock, patch
5
5
 
6
6
  import pytest
7
- from pydantic import ValidationError
7
+ from pydantic.v1 import ValidationError
8
8
 
9
9
  from cumulusci.utils import temporary_dir
10
10
  from cumulusci.utils.yaml.cumulusci_yml import (
@@ -2,7 +2,7 @@ from io import StringIO
2
2
  from unittest.mock import Mock
3
3
 
4
4
  import pytest
5
- from pydantic import Field
5
+ from pydantic.v1 import Field
6
6
 
7
7
  from cumulusci.utils.yaml.model_parser import CCIDictModel, CCIModel, ValidationError
8
8
 
@@ -20,7 +20,7 @@ class TestCCIModel:
20
20
  def test_fields_property(self):
21
21
  # JSON is YAML. Strange but true.
22
22
  foo = Document.parse_from_yaml(StringIO("{bar: 'blah'}"))
23
- assert type(foo) == Foo
23
+ assert isinstance(foo, Foo)
24
24
  assert foo.fields_ == []
25
25
  assert foo.fields == []
26
26
 
@@ -122,7 +122,7 @@ class TestCCIDictModel:
122
122
 
123
123
  # JSON is YAML. Strange but true.
124
124
  foo = Document.parse_from_yaml(StringIO("{bar: 'blah'}"))
125
- assert type(foo) == Foo
125
+ assert isinstance(foo, Foo)
126
126
  assert foo["fields"] == []
127
127
 
128
128
  foo = Document.parse_from_yaml(StringIO("{bar: 'blah', fields: [1,2]}"))
cumulusci/vcs/base.py CHANGED
@@ -1,15 +1,17 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
- from typing import List, Optional, Type
3
+ from typing import TYPE_CHECKING, List, Optional, Set, Type
4
4
 
5
- from cumulusci.core.config import BaseProjectConfig, ServiceConfig
6
- from cumulusci.core.dependencies.base import DynamicDependency
7
5
  from cumulusci.core.keychain import BaseProjectKeychain
8
- from cumulusci.tasks.release_notes.generator import BaseReleaseNotesGenerator
9
6
  from cumulusci.utils.classutils import get_all_subclasses
10
7
  from cumulusci.vcs.models import AbstractRelease, AbstractRepo
11
8
  from cumulusci.vcs.utils import AbstractCommitDir
12
9
 
10
+ if TYPE_CHECKING:
11
+ from cumulusci.core.config import BaseProjectConfig, ServiceConfig
12
+ from cumulusci.core.dependencies.base import DynamicDependency
13
+ from cumulusci.tasks.release_notes.generator import BaseReleaseNotesGenerator
14
+
13
15
 
14
16
  class VCSService(ABC):
15
17
  """This is an abstract base class for VCS services.
@@ -18,14 +20,14 @@ class VCSService(ABC):
18
20
  """
19
21
 
20
22
  logger: logging.Logger
21
- config: BaseProjectConfig
22
- service_config: ServiceConfig
23
+ config: "BaseProjectConfig"
24
+ service_config: "ServiceConfig"
23
25
  name: str
24
26
  keychain: Optional[BaseProjectKeychain]
25
27
  _service_registry: List["VCSService"] = []
26
28
 
27
29
  def __init__(
28
- self, config: BaseProjectConfig, name: Optional[str] = None, **kwargs
30
+ self, config: "BaseProjectConfig", name: Optional[str] = None, **kwargs
29
31
  ) -> None:
30
32
  """Initializes the VCS service with the given configuration, service name, and keychain.
31
33
 
@@ -35,8 +37,11 @@ class VCSService(ABC):
35
37
  **kwargs: Additional keyword arguments.
36
38
  """
37
39
  self.config = config
38
- self.service_config = config.keychain.get_service(self.service_type, name)
39
- self.name = self.service_config.name or name
40
+ if config.keychain:
41
+ self.service_config = config.keychain.get_service(self.service_type, name)
42
+ self.name = self.service_config.name
43
+ else:
44
+ self.name = name or ""
40
45
  self.keychain = config.keychain
41
46
  self.logger = kwargs.get("logger") or logging.getLogger(__name__)
42
47
 
@@ -51,11 +56,11 @@ class VCSService(ABC):
51
56
  raise NotImplementedError(
52
57
  "Subclasses should define the service_type property"
53
58
  )
54
- return self.__class__.service_type
59
+ return str(self.__class__.service_type)
55
60
 
56
61
  @property
57
62
  @abstractmethod
58
- def dynamic_dependency_class(self) -> Type[DynamicDependency]:
63
+ def dynamic_dependency_class(self) -> Type["DynamicDependency"]:
59
64
  """Returns the dynamic dependency class for the VCS service.
60
65
  This property should be overridden by subclasses to provide
61
66
  the specific dynamic dependency class. For example, it could
@@ -78,7 +83,10 @@ class VCSService(ABC):
78
83
  @classmethod
79
84
  @abstractmethod
80
85
  def get_service_for_url(
81
- cls, project_config: BaseProjectConfig, url: str, service_alias: str = None
86
+ cls,
87
+ project_config: "BaseProjectConfig",
88
+ url: str,
89
+ service_alias: Optional[str] = None,
82
90
  ) -> Optional["VCSService"]:
83
91
  """Returns the service configuration for the given URL.
84
92
  This method should be overridden by subclasses to provide
@@ -87,7 +95,7 @@ class VCSService(ABC):
87
95
  raise NotImplementedError("Subclasses should provide their own implementation")
88
96
 
89
97
  @classmethod
90
- def registered_services(cls) -> List[Type["VCSService"]]:
98
+ def registered_services(cls) -> Set[Type["VCSService"]]:
91
99
  """This method returns all subclasses of VCSService that have been registered.
92
100
  It can be used to dynamically discover available VCS services."""
93
101
  return get_all_subclasses(cls)
@@ -127,7 +135,7 @@ class VCSService(ABC):
127
135
  raise NotImplementedError("Subclasses should provide their own implementation")
128
136
 
129
137
  @abstractmethod
130
- def release_notes_generator(self, options: dict) -> BaseReleaseNotesGenerator:
138
+ def release_notes_generator(self, options: dict) -> "BaseReleaseNotesGenerator":
131
139
  """Returns the release notes generator for the VCS service."""
132
140
  raise NotImplementedError(
133
141
  "Subclasses should define the release_notes_generator property"
@@ -136,7 +144,7 @@ class VCSService(ABC):
136
144
  @abstractmethod
137
145
  def parent_pr_notes_generator(
138
146
  self, repo: AbstractRepo
139
- ) -> BaseReleaseNotesGenerator:
147
+ ) -> "BaseReleaseNotesGenerator":
140
148
  """Returns the parent PR notes generator for the VCS service."""
141
149
  raise NotImplementedError(
142
150
  "Subclasses should define the parent_pr_notes_generator property"
@@ -226,20 +226,21 @@ def get_repo_from_config(config: BaseProjectConfig, options: dict = {}) -> Abstr
226
226
 
227
227
  def get_latest_tag(repo: AbstractRepo, beta: bool = False) -> str:
228
228
  """Query Github Releases to find the latest production or beta tag"""
229
- prefix = repo.project_config.project__git__prefix_release
229
+ prefix = repo.project_config.project__git__prefix_beta if beta else repo.project_config.project__git__prefix_release # type: ignore
230
230
 
231
231
  try:
232
232
  if not beta:
233
233
  release: Optional[AbstractRelease] = repo.latest_release()
234
234
 
235
+ if release is None:
236
+ raise
237
+
235
238
  if not release.tag_name.startswith(prefix):
236
239
  return _get_latest_tag_for_prefix(repo, prefix)
237
240
 
238
241
  return release.tag_name
239
242
  else:
240
- return _get_latest_tag_for_prefix(
241
- repo, repo.project_config.project__git__prefix_beta
242
- )
243
+ return _get_latest_tag_for_prefix(repo, prefix)
243
244
  except Exception:
244
245
  raise VcsException(
245
246
  f"No release found for {repo.repo_url} with tag prefix {prefix}"