cumulusci-plus 5.0.35__py3-none-any.whl → 5.0.45__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 (39) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/cci.py +3 -2
  3. cumulusci/cli/task.py +9 -10
  4. cumulusci/cli/tests/test_org.py +5 -0
  5. cumulusci/cli/tests/test_task.py +34 -0
  6. cumulusci/core/config/__init__.py +1 -0
  7. cumulusci/core/config/org_config.py +2 -1
  8. cumulusci/core/config/project_config.py +12 -0
  9. cumulusci/core/config/scratch_org_config.py +12 -0
  10. cumulusci/core/config/sfdx_org_config.py +4 -1
  11. cumulusci/core/config/tests/test_config.py +1 -0
  12. cumulusci/core/dependencies/base.py +4 -0
  13. cumulusci/cumulusci.yml +18 -1
  14. cumulusci/schema/cumulusci.jsonschema.json +5 -0
  15. cumulusci/tasks/apex/testrunner.py +7 -4
  16. cumulusci/tasks/bulkdata/tests/test_select_utils.py +20 -0
  17. cumulusci/tasks/metadata_etl/__init__.py +2 -0
  18. cumulusci/tasks/metadata_etl/applications.py +256 -0
  19. cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
  20. cumulusci/tasks/salesforce/insert_record.py +18 -19
  21. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
  22. cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
  23. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +523 -8
  24. cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
  25. cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
  26. cumulusci/tasks/salesforce/update_external_credential.py +89 -4
  27. cumulusci/tasks/salesforce/update_record.py +217 -0
  28. cumulusci/tasks/sfdmu/sfdmu.py +14 -1
  29. cumulusci/tasks/utility/credentialManager.py +58 -12
  30. cumulusci/tasks/utility/secretsToEnv.py +42 -11
  31. cumulusci/tasks/utility/tests/test_credentialManager.py +586 -0
  32. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1240 -62
  33. cumulusci/utils/yaml/cumulusci_yml.py +1 -0
  34. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/METADATA +5 -7
  35. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/RECORD +39 -33
  36. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/WHEEL +1 -1
  37. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/entry_points.txt +0 -0
  38. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/AUTHORS.rst +0 -0
  39. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,256 @@
1
+ from typing import List, Optional, Set
2
+ from urllib.parse import quote
3
+
4
+ from pydantic.v1 import BaseModel
5
+
6
+ from cumulusci.tasks.metadata_etl import MetadataSingleEntityTransformTask
7
+ from cumulusci.utils import inject_namespace
8
+ from cumulusci.utils.xml.metadata_tree import MetadataElement
9
+
10
+
11
+ def _inject_namespace(text: str, options: dict) -> str:
12
+ return inject_namespace(
13
+ "",
14
+ text,
15
+ namespace=options["namespace_inject"]
16
+ if not options.get("namespaced_org")
17
+ else "",
18
+ managed=options.get("managed") or False,
19
+ namespaced_org=options.get("namespaced_org"),
20
+ )[1]
21
+
22
+
23
+ class ProfileActionOverrideOptions(BaseModel):
24
+ """Options for a single profileActionOverride"""
25
+
26
+ action_name: str
27
+ content: str
28
+ form_factor: str
29
+ page_or_sobject_type: str
30
+ record_type: Optional[str]
31
+ type: str
32
+ profile: str
33
+ options: dict = {}
34
+
35
+ def namespace_inject(self, field_name: str) -> None:
36
+ setattr(
37
+ self, field_name, _inject_namespace(getattr(self, field_name), self.options)
38
+ )
39
+
40
+
41
+ class AddProfileActionOverridesOptions(BaseModel):
42
+ """Options container for profile action overrides"""
43
+
44
+ name: str
45
+ overrides: List[ProfileActionOverrideOptions]
46
+
47
+
48
+ class AddProfileActionOverrides(MetadataSingleEntityTransformTask):
49
+ """
50
+ Inserts or updates profileActionOverrides in CustomApplication metadata.
51
+ If a profileActionOverride with the same actionName, pageOrSobjectType,
52
+ recordType, and profile already exists, it will be updated with a warning.
53
+ Otherwise, a new override will be added.
54
+ Task option details:
55
+ - applications: List of CustomApplications to modify
56
+ - name: API name of the CustomApplication to modify
57
+ - overrides: List of profile action overrides to add/update
58
+ - action_name: Action name (e.g., "View", "Edit", "New")
59
+ - content: Content reference (page/component API name)
60
+ - form_factor: Form factor (e.g., "Large", "Small")
61
+ - page_or_sobject_type: Page or SObject type
62
+ - record_type: Record type API name (optional)
63
+ - type: Override type (e.g., "Flexipage", "Visualforce", "LightningComponent")
64
+ - profile: Profile name or API name
65
+ Example Usage
66
+ -----------------------
67
+ .. code-block:: yaml
68
+ task: add_profile_action_overrides
69
+ options:
70
+ applications:
71
+ - name: "%%%NAMESPACE%%%CustomApplicationConsole"
72
+ overrides:
73
+ - action_name: View
74
+ content: "%%%NAMESPACED_ORG%%%AccountUserRecordPage"
75
+ form_factor: Large
76
+ page_or_sobject_type: Account
77
+ record_type: PersonAccount.User
78
+ type: Flexipage
79
+ profile: Admin
80
+ """
81
+
82
+ entity = "CustomApplication"
83
+ task_options = {
84
+ "applications": {
85
+ "description": "List of CustomApplications to modify. See task info for structure.",
86
+ "required": True,
87
+ },
88
+ **MetadataSingleEntityTransformTask.task_options,
89
+ }
90
+
91
+ def _init_options(self, kwargs):
92
+ super()._init_options(kwargs)
93
+
94
+ # Validate options using Pydantic
95
+ self._validated_options: List[AddProfileActionOverridesOptions] = []
96
+ for application in self.options.get("applications"):
97
+ validated_options = AddProfileActionOverridesOptions(
98
+ name=quote(_inject_namespace(application.get("name"), self.options)),
99
+ overrides=application.get("overrides"),
100
+ )
101
+
102
+ self._validated_options.append(validated_options)
103
+
104
+ self.api_names: Set[str] = set(
105
+ application.name for application in self._validated_options
106
+ )
107
+
108
+ def _transform_entity(
109
+ self, metadata: MetadataElement, api_name: str
110
+ ) -> Optional[MetadataElement]:
111
+
112
+ if not self._validated_options:
113
+ self.logger.warning("No applications to add profile action overrides for")
114
+ return None
115
+
116
+ for application in self._validated_options:
117
+ if application.name != api_name:
118
+ continue
119
+
120
+ for override_config in application.overrides:
121
+ self._add_or_update_override(
122
+ metadata, application.name, override_config
123
+ )
124
+
125
+ return metadata
126
+
127
+ def _add_or_update_override(self, metadata, api_name, override_config):
128
+ """Add or update a single profileActionOverride"""
129
+ override_config.options = self.options
130
+
131
+ # Inject namespace where needed
132
+ override_config.namespace_inject("content")
133
+ override_config.namespace_inject("page_or_sobject_type")
134
+ override_config.namespace_inject(
135
+ "record_type"
136
+ ) if override_config.record_type else None
137
+
138
+ # Find existing override with same key fields
139
+ existing_override = self._find_existing_override(metadata, override_config)
140
+
141
+ if existing_override:
142
+ self.logger.warning(
143
+ f"Updating existing profileActionOverride for {override_config.profile}/{override_config.page_or_sobject_type}/{override_config.action_name} in {api_name}"
144
+ )
145
+ # Update the existing override
146
+ self._update_override_element(
147
+ existing_override,
148
+ override_config,
149
+ )
150
+ else:
151
+ self.logger.info(
152
+ f"Adding profileActionOverride for {override_config.profile}/{override_config.page_or_sobject_type}/{override_config.action_name} to {api_name}"
153
+ )
154
+ # Create new override
155
+ self._create_new_override(metadata, override_config)
156
+
157
+ def _find_existing_override(self, metadata, override_config):
158
+ """
159
+ Find an existing profileActionOverride that matches the key fields:
160
+ actionName, pageOrSobjectType, recordType, and profile
161
+ """
162
+ for override_elem in metadata.findall("profileActionOverrides"):
163
+ elem_action = override_elem.find("actionName")
164
+ elem_page = override_elem.find("pageOrSobjectType")
165
+ elem_record_type = override_elem.find("recordType")
166
+ elem_profile = override_elem.find("profile")
167
+
168
+ # Match on all key fields
169
+ if (
170
+ elem_action
171
+ and elem_action.text == override_config.action_name
172
+ and elem_page
173
+ and elem_page.text == override_config.page_or_sobject_type
174
+ and elem_profile
175
+ and elem_profile.text == override_config.profile
176
+ ):
177
+ # Handle recordType - both must be None or both must match
178
+ if override_config.record_type is None and elem_record_type is None:
179
+ return override_elem
180
+ elif (
181
+ override_config.record_type is not None
182
+ and elem_record_type is not None
183
+ and elem_record_type.text == override_config.record_type
184
+ ):
185
+ return override_elem
186
+
187
+ return None
188
+
189
+ def _update_override_element(
190
+ self,
191
+ override_elem,
192
+ override_config,
193
+ ):
194
+ """Update an existing profileActionOverride element"""
195
+ # Update each child element
196
+ # actionName
197
+ elem = override_elem.find("actionName")
198
+ if elem is not None:
199
+ elem.text = override_config.action_name
200
+
201
+ # content
202
+ elem = override_elem.find("content")
203
+ if elem is not None:
204
+ elem.text = override_config.content
205
+
206
+ # formFactor
207
+ elem = override_elem.find("formFactor")
208
+ if elem is not None:
209
+ elem.text = override_config.form_factor
210
+
211
+ # pageOrSobjectType
212
+ elem = override_elem.find("pageOrSobjectType")
213
+ if elem is not None:
214
+ elem.text = override_config.page_or_sobject_type
215
+
216
+ # recordType (optional)
217
+ elem = override_elem.find("recordType")
218
+ if override_config.record_type:
219
+ if elem is not None:
220
+ elem.text = override_config.record_type
221
+ elif elem is not None:
222
+ # Remove recordType if it exists but shouldn't
223
+ override_elem.remove(elem)
224
+
225
+ # type
226
+ elem = override_elem.find("type")
227
+ if elem is not None:
228
+ elem.text = override_config.type
229
+
230
+ # profile
231
+ elem = override_elem.find("profile")
232
+ if elem is not None:
233
+ elem.text = override_config.profile
234
+
235
+ def _create_new_override(
236
+ self,
237
+ metadata,
238
+ override_config,
239
+ ):
240
+ """Create a new profileActionOverride element with proper ordering"""
241
+ override_elem = metadata.append("profileActionOverrides")
242
+
243
+ # Add elements in the correct order per Salesforce metadata API
244
+ override_elem.append("actionName", text=override_config.action_name)
245
+ override_elem.append("content", text=override_config.content)
246
+ override_elem.append("formFactor", text=override_config.form_factor)
247
+ override_elem.append(
248
+ "pageOrSobjectType", text=override_config.page_or_sobject_type
249
+ )
250
+
251
+ # recordType is optional
252
+ if override_config.record_type:
253
+ override_elem.append("recordType", text=override_config.record_type)
254
+
255
+ override_elem.append("type", text=override_config.type)
256
+ override_elem.append("profile", text=override_config.profile)