cumulusci-plus 5.0.0__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.

Files changed (744) hide show
  1. cumulusci/__about__.py +1 -0
  2. cumulusci/__init__.py +22 -0
  3. cumulusci/__main__.py +3 -0
  4. cumulusci/cli/__init__.py +0 -0
  5. cumulusci/cli/cci.py +244 -0
  6. cumulusci/cli/error.py +125 -0
  7. cumulusci/cli/flow.py +185 -0
  8. cumulusci/cli/logger.py +72 -0
  9. cumulusci/cli/org.py +692 -0
  10. cumulusci/cli/plan.py +181 -0
  11. cumulusci/cli/project.py +391 -0
  12. cumulusci/cli/robot.py +116 -0
  13. cumulusci/cli/runtime.py +190 -0
  14. cumulusci/cli/service.py +521 -0
  15. cumulusci/cli/task.py +295 -0
  16. cumulusci/cli/tests/__init__.py +0 -0
  17. cumulusci/cli/tests/test_cci.py +545 -0
  18. cumulusci/cli/tests/test_error.py +170 -0
  19. cumulusci/cli/tests/test_flow.py +276 -0
  20. cumulusci/cli/tests/test_logger.py +25 -0
  21. cumulusci/cli/tests/test_org.py +1438 -0
  22. cumulusci/cli/tests/test_plan.py +245 -0
  23. cumulusci/cli/tests/test_project.py +235 -0
  24. cumulusci/cli/tests/test_robot.py +177 -0
  25. cumulusci/cli/tests/test_runtime.py +197 -0
  26. cumulusci/cli/tests/test_service.py +853 -0
  27. cumulusci/cli/tests/test_task.py +266 -0
  28. cumulusci/cli/tests/test_ui.py +310 -0
  29. cumulusci/cli/tests/test_utils.py +122 -0
  30. cumulusci/cli/tests/utils.py +52 -0
  31. cumulusci/cli/ui.py +234 -0
  32. cumulusci/cli/utils.py +150 -0
  33. cumulusci/conftest.py +181 -0
  34. cumulusci/core/__init__.py +0 -0
  35. cumulusci/core/config/BaseConfig.py +5 -0
  36. cumulusci/core/config/BaseTaskFlowConfig.py +5 -0
  37. cumulusci/core/config/OrgConfig.py +5 -0
  38. cumulusci/core/config/ScratchOrgConfig.py +5 -0
  39. cumulusci/core/config/__init__.py +125 -0
  40. cumulusci/core/config/base_config.py +111 -0
  41. cumulusci/core/config/base_task_flow_config.py +82 -0
  42. cumulusci/core/config/marketing_cloud_service_config.py +83 -0
  43. cumulusci/core/config/oauth2_service_config.py +17 -0
  44. cumulusci/core/config/org_config.py +604 -0
  45. cumulusci/core/config/project_config.py +782 -0
  46. cumulusci/core/config/scratch_org_config.py +251 -0
  47. cumulusci/core/config/sfdx_org_config.py +220 -0
  48. cumulusci/core/config/tests/_test_config_backwards_compatibility.py +33 -0
  49. cumulusci/core/config/tests/test_config.py +1895 -0
  50. cumulusci/core/config/tests/test_config_expensive.py +839 -0
  51. cumulusci/core/config/tests/test_config_util.py +91 -0
  52. cumulusci/core/config/universal_config.py +88 -0
  53. cumulusci/core/config/util.py +18 -0
  54. cumulusci/core/datasets.py +303 -0
  55. cumulusci/core/debug.py +33 -0
  56. cumulusci/core/dependencies/__init__.py +55 -0
  57. cumulusci/core/dependencies/base.py +561 -0
  58. cumulusci/core/dependencies/dependencies.py +273 -0
  59. cumulusci/core/dependencies/github.py +177 -0
  60. cumulusci/core/dependencies/github_resolvers.py +244 -0
  61. cumulusci/core/dependencies/resolvers.py +580 -0
  62. cumulusci/core/dependencies/tests/__init__.py +0 -0
  63. cumulusci/core/dependencies/tests/conftest.py +385 -0
  64. cumulusci/core/dependencies/tests/test_dependencies.py +950 -0
  65. cumulusci/core/dependencies/tests/test_github.py +83 -0
  66. cumulusci/core/dependencies/tests/test_resolvers.py +1027 -0
  67. cumulusci/core/dependencies/utils.py +13 -0
  68. cumulusci/core/enums.py +11 -0
  69. cumulusci/core/exceptions.py +311 -0
  70. cumulusci/core/flowrunner.py +888 -0
  71. cumulusci/core/github.py +665 -0
  72. cumulusci/core/keychain/__init__.py +24 -0
  73. cumulusci/core/keychain/base_project_keychain.py +441 -0
  74. cumulusci/core/keychain/encrypted_file_project_keychain.py +945 -0
  75. cumulusci/core/keychain/environment_project_keychain.py +7 -0
  76. cumulusci/core/keychain/serialization.py +152 -0
  77. cumulusci/core/keychain/subprocess_keychain.py +24 -0
  78. cumulusci/core/keychain/tests/conftest.py +50 -0
  79. cumulusci/core/keychain/tests/test_base_project_keychain.py +299 -0
  80. cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +1228 -0
  81. cumulusci/core/metadeploy/__init__.py +0 -0
  82. cumulusci/core/metadeploy/api.py +88 -0
  83. cumulusci/core/metadeploy/plans.py +25 -0
  84. cumulusci/core/metadeploy/tests/test_api.py +276 -0
  85. cumulusci/core/runtime.py +115 -0
  86. cumulusci/core/sfdx.py +162 -0
  87. cumulusci/core/source/__init__.py +16 -0
  88. cumulusci/core/source/github.py +50 -0
  89. cumulusci/core/source/local_folder.py +35 -0
  90. cumulusci/core/source_transforms/__init__.py +0 -0
  91. cumulusci/core/source_transforms/tests/test_transforms.py +1091 -0
  92. cumulusci/core/source_transforms/transforms.py +532 -0
  93. cumulusci/core/tasks.py +404 -0
  94. cumulusci/core/template_utils.py +59 -0
  95. cumulusci/core/tests/__init__.py +0 -0
  96. cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_e2e.yaml +215 -0
  97. cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_extract_standard_objects.yaml +199 -0
  98. cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_read_explicit_extract_declaration.yaml +3 -0
  99. cumulusci/core/tests/fake_remote_repo/cumulusci.yml +32 -0
  100. cumulusci/core/tests/fake_remote_repo/tasks/directory/example_2.py +6 -0
  101. cumulusci/core/tests/fake_remote_repo/tasks/example.py +43 -0
  102. cumulusci/core/tests/fake_remote_repo_2/cumulusci.yml +11 -0
  103. cumulusci/core/tests/fake_remote_repo_2/tasks/example_3.py +6 -0
  104. cumulusci/core/tests/test_datasets_e2e.py +386 -0
  105. cumulusci/core/tests/test_exceptions.py +11 -0
  106. cumulusci/core/tests/test_flowrunner.py +836 -0
  107. cumulusci/core/tests/test_github.py +942 -0
  108. cumulusci/core/tests/test_sfdx.py +138 -0
  109. cumulusci/core/tests/test_source.py +678 -0
  110. cumulusci/core/tests/test_tasks.py +262 -0
  111. cumulusci/core/tests/test_utils.py +141 -0
  112. cumulusci/core/tests/test_utils_merge_config.py +276 -0
  113. cumulusci/core/tests/test_versions.py +76 -0
  114. cumulusci/core/tests/untrusted_repo_child/cumulusci.yml +7 -0
  115. cumulusci/core/tests/untrusted_repo_child/tasks/untrusted_child.py +6 -0
  116. cumulusci/core/tests/untrusted_repo_parent/cumulusci.yml +26 -0
  117. cumulusci/core/tests/untrusted_repo_parent/tasks/untrusted_parent.py +6 -0
  118. cumulusci/core/tests/utils.py +116 -0
  119. cumulusci/core/tests/yaml/global.yaml +0 -0
  120. cumulusci/core/utils.py +402 -0
  121. cumulusci/core/versions.py +149 -0
  122. cumulusci/cumulusci.yml +1621 -0
  123. cumulusci/files/admin_profile.xml +20 -0
  124. cumulusci/files/delete_excludes.txt +424 -0
  125. cumulusci/files/templates/project/README.md +12 -0
  126. cumulusci/files/templates/project/cumulusci.yml +63 -0
  127. cumulusci/files/templates/project/dot-gitignore +60 -0
  128. cumulusci/files/templates/project/mapping.yml +45 -0
  129. cumulusci/files/templates/project/scratch_def.json +25 -0
  130. cumulusci/oauth/__init__.py +0 -0
  131. cumulusci/oauth/client.py +400 -0
  132. cumulusci/oauth/exceptions.py +9 -0
  133. cumulusci/oauth/salesforce.py +95 -0
  134. cumulusci/oauth/tests/__init__.py +0 -0
  135. cumulusci/oauth/tests/cassettes/test_get_device_code.yaml +22 -0
  136. cumulusci/oauth/tests/cassettes/test_get_device_oauth_token.yaml +74 -0
  137. cumulusci/oauth/tests/test_client.py +308 -0
  138. cumulusci/oauth/tests/test_salesforce.py +46 -0
  139. cumulusci/plugins/__init__.py +3 -0
  140. cumulusci/plugins/plugin_base.py +93 -0
  141. cumulusci/plugins/plugin_loader.py +59 -0
  142. cumulusci/robotframework/CumulusCI.py +340 -0
  143. cumulusci/robotframework/CumulusCI.robot +7 -0
  144. cumulusci/robotframework/Performance.py +165 -0
  145. cumulusci/robotframework/Salesforce.py +936 -0
  146. cumulusci/robotframework/Salesforce.robot +192 -0
  147. cumulusci/robotframework/SalesforceAPI.py +416 -0
  148. cumulusci/robotframework/SalesforcePlaywright.py +220 -0
  149. cumulusci/robotframework/SalesforcePlaywright.robot +40 -0
  150. cumulusci/robotframework/__init__.py +2 -0
  151. cumulusci/robotframework/base_library.py +39 -0
  152. cumulusci/robotframework/faker_mixin.py +89 -0
  153. cumulusci/robotframework/form_handlers.py +222 -0
  154. cumulusci/robotframework/javascript/cci_init.js +34 -0
  155. cumulusci/robotframework/javascript/cumulusci.js +4 -0
  156. cumulusci/robotframework/locator_manager.py +197 -0
  157. cumulusci/robotframework/locators_56.py +88 -0
  158. cumulusci/robotframework/locators_57.py +5 -0
  159. cumulusci/robotframework/pageobjects/BasePageObjects.py +433 -0
  160. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +246 -0
  161. cumulusci/robotframework/pageobjects/PageObjectLibrary.py +45 -0
  162. cumulusci/robotframework/pageobjects/PageObjects.py +351 -0
  163. cumulusci/robotframework/pageobjects/__init__.py +12 -0
  164. cumulusci/robotframework/pageobjects/baseobjects.py +120 -0
  165. cumulusci/robotframework/perftests/short/collection_perf.robot +105 -0
  166. cumulusci/robotframework/tests/CustomObjectTestPage.py +10 -0
  167. cumulusci/robotframework/tests/FooTestPage.py +8 -0
  168. cumulusci/robotframework/tests/cumulusci/base.robot +40 -0
  169. cumulusci/robotframework/tests/cumulusci/bulkdata.robot +38 -0
  170. cumulusci/robotframework/tests/cumulusci/communities.robot +57 -0
  171. cumulusci/robotframework/tests/cumulusci/datagen.robot +84 -0
  172. cumulusci/robotframework/tests/salesforce/TestLibraryA.py +24 -0
  173. cumulusci/robotframework/tests/salesforce/TestLibraryB.py +20 -0
  174. cumulusci/robotframework/tests/salesforce/TestListener.py +93 -0
  175. cumulusci/robotframework/tests/salesforce/api.robot +178 -0
  176. cumulusci/robotframework/tests/salesforce/browsers.robot +143 -0
  177. cumulusci/robotframework/tests/salesforce/classic.robot +51 -0
  178. cumulusci/robotframework/tests/salesforce/create_contact.robot +59 -0
  179. cumulusci/robotframework/tests/salesforce/faker.robot +68 -0
  180. cumulusci/robotframework/tests/salesforce/forms.robot +172 -0
  181. cumulusci/robotframework/tests/salesforce/label_locator.robot +244 -0
  182. cumulusci/robotframework/tests/salesforce/labels.html +33 -0
  183. cumulusci/robotframework/tests/salesforce/locators.robot +149 -0
  184. cumulusci/robotframework/tests/salesforce/pageobjects/base_pageobjects.robot +100 -0
  185. cumulusci/robotframework/tests/salesforce/pageobjects/example_page_object.py +25 -0
  186. cumulusci/robotframework/tests/salesforce/pageobjects/listing_page.robot +115 -0
  187. cumulusci/robotframework/tests/salesforce/pageobjects/objectmanager.robot +74 -0
  188. cumulusci/robotframework/tests/salesforce/pageobjects/pageobjects.robot +171 -0
  189. cumulusci/robotframework/tests/salesforce/performance.robot +109 -0
  190. cumulusci/robotframework/tests/salesforce/playwright/javascript_keywords.robot +33 -0
  191. cumulusci/robotframework/tests/salesforce/playwright/open_test_browser.robot +48 -0
  192. cumulusci/robotframework/tests/salesforce/playwright/playwright.robot +24 -0
  193. cumulusci/robotframework/tests/salesforce/playwright/ui.robot +32 -0
  194. cumulusci/robotframework/tests/salesforce/populate.robot +89 -0
  195. cumulusci/robotframework/tests/salesforce/test_testlistener.py +37 -0
  196. cumulusci/robotframework/tests/salesforce/ui.robot +361 -0
  197. cumulusci/robotframework/tests/test_cumulusci_library.py +304 -0
  198. cumulusci/robotframework/tests/test_locator_manager.py +158 -0
  199. cumulusci/robotframework/tests/test_pageobjects.py +291 -0
  200. cumulusci/robotframework/tests/test_performance.py +38 -0
  201. cumulusci/robotframework/tests/test_salesforce.py +79 -0
  202. cumulusci/robotframework/tests/test_salesforce_locators.py +73 -0
  203. cumulusci/robotframework/tests/test_template_util.py +53 -0
  204. cumulusci/robotframework/tests/test_utils.py +106 -0
  205. cumulusci/robotframework/utils.py +283 -0
  206. cumulusci/salesforce_api/__init__.py +0 -0
  207. cumulusci/salesforce_api/exceptions.py +23 -0
  208. cumulusci/salesforce_api/filterable_objects.py +96 -0
  209. cumulusci/salesforce_api/mc_soap_envelopes.py +89 -0
  210. cumulusci/salesforce_api/metadata.py +721 -0
  211. cumulusci/salesforce_api/org_schema.py +571 -0
  212. cumulusci/salesforce_api/org_schema_models.py +226 -0
  213. cumulusci/salesforce_api/package_install.py +265 -0
  214. cumulusci/salesforce_api/package_zip.py +301 -0
  215. cumulusci/salesforce_api/rest_deploy.py +148 -0
  216. cumulusci/salesforce_api/retrieve_profile_api.py +301 -0
  217. cumulusci/salesforce_api/soap_envelopes.py +177 -0
  218. cumulusci/salesforce_api/tests/__init__.py +0 -0
  219. cumulusci/salesforce_api/tests/metadata_test_strings.py +24 -0
  220. cumulusci/salesforce_api/tests/test_metadata.py +1015 -0
  221. cumulusci/salesforce_api/tests/test_package_install.py +219 -0
  222. cumulusci/salesforce_api/tests/test_package_zip.py +380 -0
  223. cumulusci/salesforce_api/tests/test_rest_deploy.py +264 -0
  224. cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +337 -0
  225. cumulusci/salesforce_api/tests/test_utils.py +124 -0
  226. cumulusci/salesforce_api/utils.py +51 -0
  227. cumulusci/schema/cumulusci.jsonschema.json +782 -0
  228. cumulusci/tasks/__init__.py +0 -0
  229. cumulusci/tasks/apex/__init__.py +0 -0
  230. cumulusci/tasks/apex/anon.py +157 -0
  231. cumulusci/tasks/apex/batch.py +180 -0
  232. cumulusci/tasks/apex/testrunner.py +835 -0
  233. cumulusci/tasks/apex/tests/cassettes/ManualEditTestApexIntegrationTests.test_run_tests__integration_test.yaml +703 -0
  234. cumulusci/tasks/apex/tests/test_apex_tasks.py +1558 -0
  235. cumulusci/tasks/base_source_control_task.py +17 -0
  236. cumulusci/tasks/bulkdata/__init__.py +15 -0
  237. cumulusci/tasks/bulkdata/base_generate_data_task.py +96 -0
  238. cumulusci/tasks/bulkdata/dates.py +97 -0
  239. cumulusci/tasks/bulkdata/delete.py +156 -0
  240. cumulusci/tasks/bulkdata/extract.py +441 -0
  241. cumulusci/tasks/bulkdata/extract_dataset_utils/calculate_dependencies.py +117 -0
  242. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +123 -0
  243. cumulusci/tasks/bulkdata/extract_dataset_utils/hardcoded_default_declarations.py +49 -0
  244. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +283 -0
  245. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +142 -0
  246. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py +575 -0
  247. cumulusci/tasks/bulkdata/factory_utils.py +134 -0
  248. cumulusci/tasks/bulkdata/generate.py +4 -0
  249. cumulusci/tasks/bulkdata/generate_and_load_data.py +232 -0
  250. cumulusci/tasks/bulkdata/generate_and_load_data_from_yaml.py +19 -0
  251. cumulusci/tasks/bulkdata/generate_from_yaml.py +183 -0
  252. cumulusci/tasks/bulkdata/generate_mapping.py +434 -0
  253. cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py +169 -0
  254. cumulusci/tasks/bulkdata/generate_mapping_utils/extract_mapping_file_generator.py +45 -0
  255. cumulusci/tasks/bulkdata/generate_mapping_utils/generate_mapping_from_declarations.py +121 -0
  256. cumulusci/tasks/bulkdata/generate_mapping_utils/load_mapping_file_generator.py +127 -0
  257. cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_generator_post_processes.py +53 -0
  258. cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_transforms.py +139 -0
  259. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py +135 -0
  260. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_load_mapping_from_declarations.py +330 -0
  261. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_generator_post_processes.py +60 -0
  262. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_transforms.py +188 -0
  263. cumulusci/tasks/bulkdata/load.py +1196 -0
  264. cumulusci/tasks/bulkdata/mapping_parser.py +811 -0
  265. cumulusci/tasks/bulkdata/query_transformers.py +264 -0
  266. cumulusci/tasks/bulkdata/select_utils.py +792 -0
  267. cumulusci/tasks/bulkdata/snowfakery.py +753 -0
  268. cumulusci/tasks/bulkdata/snowfakery_utils/queue_manager.py +478 -0
  269. cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_run_until.py +141 -0
  270. cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_working_directory.py +53 -0
  271. cumulusci/tasks/bulkdata/snowfakery_utils/subtask_configurator.py +64 -0
  272. cumulusci/tasks/bulkdata/step.py +1242 -0
  273. cumulusci/tasks/bulkdata/tests/__init__.py +0 -0
  274. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_random_strategy.yaml +147 -0
  275. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_annoy_strategy.yaml +123 -0
  276. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy.yaml +313 -0
  277. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy_bulk.yaml +550 -0
  278. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_strategy.yaml +175 -0
  279. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_standard_strategy.yaml +147 -0
  280. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__multiple_needed.yaml +69 -0
  281. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__none_needed.yaml +22 -0
  282. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__one_needed.yaml +24 -0
  283. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_snowfakery_query_salesforce.yaml +25 -0
  284. cumulusci/tasks/bulkdata/tests/cassettes/TestUpdatesIntegrationTests.test_updates_task.yaml +80 -0
  285. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_simple_upsert__rest.yaml +270 -0
  286. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert__rest.yaml +267 -0
  287. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field__rest.yaml +369 -0
  288. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field_rest__duplicate_error.yaml +204 -0
  289. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_fields__bulk.yaml +675 -0
  290. cumulusci/tasks/bulkdata/tests/dummy_data_factory.py +36 -0
  291. cumulusci/tasks/bulkdata/tests/integration_test_utils.py +49 -0
  292. cumulusci/tasks/bulkdata/tests/mapping-oid.yml +87 -0
  293. cumulusci/tasks/bulkdata/tests/mapping_after.yml +38 -0
  294. cumulusci/tasks/bulkdata/tests/mapping_poly.yml +34 -0
  295. cumulusci/tasks/bulkdata/tests/mapping_poly_incomplete.yml +20 -0
  296. cumulusci/tasks/bulkdata/tests/mapping_poly_wrong.yml +21 -0
  297. cumulusci/tasks/bulkdata/tests/mapping_select.yml +20 -0
  298. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_strategy.yml +20 -0
  299. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_number.yml +21 -0
  300. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_strategy.yml +21 -0
  301. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__non_float.yml +21 -0
  302. cumulusci/tasks/bulkdata/tests/mapping_select_missing_priority_fields.yml +22 -0
  303. cumulusci/tasks/bulkdata/tests/mapping_select_no_priority_fields.yml +18 -0
  304. cumulusci/tasks/bulkdata/tests/mapping_simple.yml +27 -0
  305. cumulusci/tasks/bulkdata/tests/mapping_v1.yml +28 -0
  306. cumulusci/tasks/bulkdata/tests/mapping_v2.yml +21 -0
  307. cumulusci/tasks/bulkdata/tests/mapping_v3.yml +32 -0
  308. cumulusci/tasks/bulkdata/tests/mapping_vanilla_sf.yml +69 -0
  309. cumulusci/tasks/bulkdata/tests/mock_data_factory_without_mapping.py +12 -0
  310. cumulusci/tasks/bulkdata/tests/person_accounts.yml +23 -0
  311. cumulusci/tasks/bulkdata/tests/person_accounts_minimal.yml +15 -0
  312. cumulusci/tasks/bulkdata/tests/recordtypes.yml +8 -0
  313. cumulusci/tasks/bulkdata/tests/recordtypes_2.yml +6 -0
  314. cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.yml +8 -0
  315. cumulusci/tasks/bulkdata/tests/snowfakery/child/child2.yml +3 -0
  316. cumulusci/tasks/bulkdata/tests/snowfakery/child.yml +4 -0
  317. cumulusci/tasks/bulkdata/tests/snowfakery/gen_npsp_standard_objects.recipe.yml +89 -0
  318. cumulusci/tasks/bulkdata/tests/snowfakery/include_parent.yml +3 -0
  319. cumulusci/tasks/bulkdata/tests/snowfakery/npsp_standard_objects_macros.yml +34 -0
  320. cumulusci/tasks/bulkdata/tests/snowfakery/options.recipe.yml +6 -0
  321. cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml +16 -0
  322. cumulusci/tasks/bulkdata/tests/snowfakery/sf_standard_object_macros.yml +83 -0
  323. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.load.yml +2 -0
  324. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.recipe.yml +13 -0
  325. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_2.load.yml +5 -0
  326. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.load.yml +13 -0
  327. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.recipe.yml +12 -0
  328. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels_2.load.yml +13 -0
  329. cumulusci/tasks/bulkdata/tests/snowfakery/unique_values.recipe.yml +4 -0
  330. cumulusci/tasks/bulkdata/tests/snowfakery/upsert.recipe.yml +23 -0
  331. cumulusci/tasks/bulkdata/tests/snowfakery/upsert_2.recipe.yml +29 -0
  332. cumulusci/tasks/bulkdata/tests/snowfakery/upsert_before.yml +10 -0
  333. cumulusci/tasks/bulkdata/tests/test_base_generate_data_tasks.py +61 -0
  334. cumulusci/tasks/bulkdata/tests/test_dates.py +99 -0
  335. cumulusci/tasks/bulkdata/tests/test_delete.py +404 -0
  336. cumulusci/tasks/bulkdata/tests/test_extract.py +1311 -0
  337. cumulusci/tasks/bulkdata/tests/test_factory_utils.py +55 -0
  338. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +252 -0
  339. cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py +343 -0
  340. cumulusci/tasks/bulkdata/tests/test_generatemapping.py +1039 -0
  341. cumulusci/tasks/bulkdata/tests/test_load.py +3175 -0
  342. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +1658 -0
  343. cumulusci/tasks/bulkdata/tests/test_query_db__joins_self_lookups.yml +12 -0
  344. cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups.yml +26 -0
  345. cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups_select.yml +48 -0
  346. cumulusci/tasks/bulkdata/tests/test_select.py +171 -0
  347. cumulusci/tasks/bulkdata/tests/test_select_utils.py +1057 -0
  348. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +1153 -0
  349. cumulusci/tasks/bulkdata/tests/test_step.py +3957 -0
  350. cumulusci/tasks/bulkdata/tests/test_updates.py +513 -0
  351. cumulusci/tasks/bulkdata/tests/test_upsert.py +1015 -0
  352. cumulusci/tasks/bulkdata/tests/test_utils.py +158 -0
  353. cumulusci/tasks/bulkdata/tests/testdata.db +0 -0
  354. cumulusci/tasks/bulkdata/tests/update_describe.py +50 -0
  355. cumulusci/tasks/bulkdata/tests/update_person_accounts.yml +23 -0
  356. cumulusci/tasks/bulkdata/tests/utils.py +114 -0
  357. cumulusci/tasks/bulkdata/update_data.py +260 -0
  358. cumulusci/tasks/bulkdata/upsert_utils.py +130 -0
  359. cumulusci/tasks/bulkdata/utils.py +249 -0
  360. cumulusci/tasks/command.py +178 -0
  361. cumulusci/tasks/connectedapp.py +186 -0
  362. cumulusci/tasks/create_package_version.py +778 -0
  363. cumulusci/tasks/datadictionary.py +745 -0
  364. cumulusci/tasks/dx_convert_from.py +26 -0
  365. cumulusci/tasks/github/__init__.py +17 -0
  366. cumulusci/tasks/github/base.py +16 -0
  367. cumulusci/tasks/github/commit_status.py +13 -0
  368. cumulusci/tasks/github/merge.py +11 -0
  369. cumulusci/tasks/github/publish.py +11 -0
  370. cumulusci/tasks/github/pull_request.py +11 -0
  371. cumulusci/tasks/github/release.py +11 -0
  372. cumulusci/tasks/github/release_report.py +11 -0
  373. cumulusci/tasks/github/tag.py +11 -0
  374. cumulusci/tasks/github/tests/__init__.py +0 -0
  375. cumulusci/tasks/github/tests/test_util.py +202 -0
  376. cumulusci/tasks/github/tests/test_vcs_migration.py +44 -0
  377. cumulusci/tasks/github/tests/util_github_api.py +666 -0
  378. cumulusci/tasks/github/util.py +252 -0
  379. cumulusci/tasks/marketing_cloud/__init__.py +0 -0
  380. cumulusci/tasks/marketing_cloud/api.py +188 -0
  381. cumulusci/tasks/marketing_cloud/base.py +38 -0
  382. cumulusci/tasks/marketing_cloud/deploy.py +345 -0
  383. cumulusci/tasks/marketing_cloud/get_user_info.py +40 -0
  384. cumulusci/tasks/marketing_cloud/mc_constants.py +1 -0
  385. cumulusci/tasks/marketing_cloud/tests/__init__.py +0 -0
  386. cumulusci/tasks/marketing_cloud/tests/conftest.py +46 -0
  387. cumulusci/tasks/marketing_cloud/tests/expected-payload.json +110 -0
  388. cumulusci/tasks/marketing_cloud/tests/test_api.py +97 -0
  389. cumulusci/tasks/marketing_cloud/tests/test_api_soap_envelopes.py +145 -0
  390. cumulusci/tasks/marketing_cloud/tests/test_base.py +14 -0
  391. cumulusci/tasks/marketing_cloud/tests/test_deploy.py +400 -0
  392. cumulusci/tasks/marketing_cloud/tests/test_get_user_info.py +141 -0
  393. cumulusci/tasks/marketing_cloud/tests/validation-response.json +39 -0
  394. cumulusci/tasks/metadata/__init__.py +0 -0
  395. cumulusci/tasks/metadata/ee_src.py +94 -0
  396. cumulusci/tasks/metadata/managed_src.py +100 -0
  397. cumulusci/tasks/metadata/metadata_map.yml +868 -0
  398. cumulusci/tasks/metadata/modify.py +99 -0
  399. cumulusci/tasks/metadata/package.py +684 -0
  400. cumulusci/tasks/metadata/tests/__init__.py +0 -0
  401. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/.hidden/.keep +0 -0
  402. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/destructiveChanges.xml +9 -0
  403. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package.xml +9 -0
  404. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package_install_uninstall.xml +11 -0
  405. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/reports/namespace__TestFolder/TestReport.report +3 -0
  406. cumulusci/tasks/metadata/tests/sample_package.xml +9 -0
  407. cumulusci/tasks/metadata/tests/test_ee_src.py +112 -0
  408. cumulusci/tasks/metadata/tests/test_managed_src.py +111 -0
  409. cumulusci/tasks/metadata/tests/test_modify.py +123 -0
  410. cumulusci/tasks/metadata/tests/test_package.py +476 -0
  411. cumulusci/tasks/metadata_etl/__init__.py +29 -0
  412. cumulusci/tasks/metadata_etl/base.py +436 -0
  413. cumulusci/tasks/metadata_etl/duplicate_rules.py +24 -0
  414. cumulusci/tasks/metadata_etl/field_sets.py +70 -0
  415. cumulusci/tasks/metadata_etl/help_text.py +92 -0
  416. cumulusci/tasks/metadata_etl/layouts.py +550 -0
  417. cumulusci/tasks/metadata_etl/objects.py +68 -0
  418. cumulusci/tasks/metadata_etl/permissions.py +167 -0
  419. cumulusci/tasks/metadata_etl/picklists.py +221 -0
  420. cumulusci/tasks/metadata_etl/remote_site_settings.py +99 -0
  421. cumulusci/tasks/metadata_etl/sharing.py +138 -0
  422. cumulusci/tasks/metadata_etl/tests/test_base.py +512 -0
  423. cumulusci/tasks/metadata_etl/tests/test_duplicate_rules.py +22 -0
  424. cumulusci/tasks/metadata_etl/tests/test_field_sets.py +156 -0
  425. cumulusci/tasks/metadata_etl/tests/test_help_text.py +387 -0
  426. cumulusci/tasks/metadata_etl/tests/test_ip_ranges.py +85 -0
  427. cumulusci/tasks/metadata_etl/tests/test_layouts.py +858 -0
  428. cumulusci/tasks/metadata_etl/tests/test_objects.py +236 -0
  429. cumulusci/tasks/metadata_etl/tests/test_permissions.py +223 -0
  430. cumulusci/tasks/metadata_etl/tests/test_picklists.py +547 -0
  431. cumulusci/tasks/metadata_etl/tests/test_remote_site_settings.py +46 -0
  432. cumulusci/tasks/metadata_etl/tests/test_sharing.py +333 -0
  433. cumulusci/tasks/metadata_etl/tests/test_value_sets.py +298 -0
  434. cumulusci/tasks/metadata_etl/value_sets.py +106 -0
  435. cumulusci/tasks/metadeploy.py +393 -0
  436. cumulusci/tasks/metaxml.py +88 -0
  437. cumulusci/tasks/preflight/__init__.py +0 -0
  438. cumulusci/tasks/preflight/dataset_load.py +49 -0
  439. cumulusci/tasks/preflight/licenses.py +86 -0
  440. cumulusci/tasks/preflight/packages.py +14 -0
  441. cumulusci/tasks/preflight/permsets.py +23 -0
  442. cumulusci/tasks/preflight/recordtypes.py +16 -0
  443. cumulusci/tasks/preflight/retrieve_tasks.py +30 -0
  444. cumulusci/tasks/preflight/settings.py +77 -0
  445. cumulusci/tasks/preflight/sobjects.py +202 -0
  446. cumulusci/tasks/preflight/tests/test_dataset_load.py +85 -0
  447. cumulusci/tasks/preflight/tests/test_licenses.py +174 -0
  448. cumulusci/tasks/preflight/tests/test_packages.py +14 -0
  449. cumulusci/tasks/preflight/tests/test_permset_preflights.py +51 -0
  450. cumulusci/tasks/preflight/tests/test_recordtypes.py +30 -0
  451. cumulusci/tasks/preflight/tests/test_retrieve_tasks.py +62 -0
  452. cumulusci/tasks/preflight/tests/test_settings.py +130 -0
  453. cumulusci/tasks/preflight/tests/test_sobjects.py +231 -0
  454. cumulusci/tasks/push/README.md +59 -0
  455. cumulusci/tasks/push/__init__.py +0 -0
  456. cumulusci/tasks/push/push_api.py +659 -0
  457. cumulusci/tasks/push/pushfails.py +136 -0
  458. cumulusci/tasks/push/tasks.py +476 -0
  459. cumulusci/tasks/push/tests/conftest.py +263 -0
  460. cumulusci/tasks/push/tests/test_push_api.py +951 -0
  461. cumulusci/tasks/push/tests/test_push_tasks.py +659 -0
  462. cumulusci/tasks/release_notes/README.md +63 -0
  463. cumulusci/tasks/release_notes/__init__.py +0 -0
  464. cumulusci/tasks/release_notes/exceptions.py +5 -0
  465. cumulusci/tasks/release_notes/generator.py +137 -0
  466. cumulusci/tasks/release_notes/parser.py +232 -0
  467. cumulusci/tasks/release_notes/provider.py +44 -0
  468. cumulusci/tasks/release_notes/task.py +300 -0
  469. cumulusci/tasks/release_notes/tests/__init__.py +0 -0
  470. cumulusci/tasks/release_notes/tests/change_notes/full/example1.md +17 -0
  471. cumulusci/tasks/release_notes/tests/change_notes/multi/1.txt +1 -0
  472. cumulusci/tasks/release_notes/tests/change_notes/multi/2.txt +1 -0
  473. cumulusci/tasks/release_notes/tests/change_notes/multi/3.txt +1 -0
  474. cumulusci/tasks/release_notes/tests/change_notes/single/1.txt +1 -0
  475. cumulusci/tasks/release_notes/tests/test_generator.py +582 -0
  476. cumulusci/tasks/release_notes/tests/test_parser.py +867 -0
  477. cumulusci/tasks/release_notes/tests/test_provider.py +512 -0
  478. cumulusci/tasks/release_notes/tests/test_task.py +461 -0
  479. cumulusci/tasks/release_notes/tests/utils.py +153 -0
  480. cumulusci/tasks/robotframework/__init__.py +3 -0
  481. cumulusci/tasks/robotframework/debugger/DebugListener.py +100 -0
  482. cumulusci/tasks/robotframework/debugger/__init__.py +10 -0
  483. cumulusci/tasks/robotframework/debugger/model.py +87 -0
  484. cumulusci/tasks/robotframework/debugger/ui.py +259 -0
  485. cumulusci/tasks/robotframework/libdoc.py +269 -0
  486. cumulusci/tasks/robotframework/robotframework.py +392 -0
  487. cumulusci/tasks/robotframework/stylesheet.css +130 -0
  488. cumulusci/tasks/robotframework/template.html +109 -0
  489. cumulusci/tasks/robotframework/tests/TestLibrary.py +18 -0
  490. cumulusci/tasks/robotframework/tests/TestPageObjects.py +31 -0
  491. cumulusci/tasks/robotframework/tests/TestResource.robot +8 -0
  492. cumulusci/tasks/robotframework/tests/failing_tests.robot +16 -0
  493. cumulusci/tasks/robotframework/tests/performance.robot +23 -0
  494. cumulusci/tasks/robotframework/tests/test_browser_proxies.py +137 -0
  495. cumulusci/tasks/robotframework/tests/test_debugger.py +360 -0
  496. cumulusci/tasks/robotframework/tests/test_robot_parallel.py +141 -0
  497. cumulusci/tasks/robotframework/tests/test_robotframework.py +860 -0
  498. cumulusci/tasks/salesforce/BaseRetrieveMetadata.py +58 -0
  499. cumulusci/tasks/salesforce/BaseSalesforceApiTask.py +45 -0
  500. cumulusci/tasks/salesforce/BaseSalesforceMetadataApiTask.py +18 -0
  501. cumulusci/tasks/salesforce/BaseSalesforceTask.py +4 -0
  502. cumulusci/tasks/salesforce/BaseUninstallMetadata.py +41 -0
  503. cumulusci/tasks/salesforce/CreateCommunity.py +124 -0
  504. cumulusci/tasks/salesforce/CreatePackage.py +29 -0
  505. cumulusci/tasks/salesforce/Deploy.py +240 -0
  506. cumulusci/tasks/salesforce/DeployBundles.py +88 -0
  507. cumulusci/tasks/salesforce/DescribeMetadataTypes.py +26 -0
  508. cumulusci/tasks/salesforce/EnsureRecordTypes.py +202 -0
  509. cumulusci/tasks/salesforce/GetInstalledPackages.py +8 -0
  510. cumulusci/tasks/salesforce/ListCommunities.py +40 -0
  511. cumulusci/tasks/salesforce/ListCommunityTemplates.py +19 -0
  512. cumulusci/tasks/salesforce/PublishCommunity.py +62 -0
  513. cumulusci/tasks/salesforce/RetrievePackaged.py +41 -0
  514. cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py +82 -0
  515. cumulusci/tasks/salesforce/RetrieveUnpackaged.py +36 -0
  516. cumulusci/tasks/salesforce/SOQLQuery.py +39 -0
  517. cumulusci/tasks/salesforce/UninstallLocal.py +15 -0
  518. cumulusci/tasks/salesforce/UninstallLocalBundles.py +28 -0
  519. cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py +58 -0
  520. cumulusci/tasks/salesforce/UninstallPackage.py +32 -0
  521. cumulusci/tasks/salesforce/UninstallPackaged.py +56 -0
  522. cumulusci/tasks/salesforce/UpdateAdminProfile.py +8 -0
  523. cumulusci/tasks/salesforce/__init__.py +79 -0
  524. cumulusci/tasks/salesforce/activate_flow.py +74 -0
  525. cumulusci/tasks/salesforce/check_components.py +324 -0
  526. cumulusci/tasks/salesforce/composite.py +142 -0
  527. cumulusci/tasks/salesforce/create_permission_sets.py +35 -0
  528. cumulusci/tasks/salesforce/custom_settings.py +134 -0
  529. cumulusci/tasks/salesforce/custom_settings_wait.py +132 -0
  530. cumulusci/tasks/salesforce/enable_prediction.py +107 -0
  531. cumulusci/tasks/salesforce/insert_record.py +40 -0
  532. cumulusci/tasks/salesforce/install_package_version.py +242 -0
  533. cumulusci/tasks/salesforce/license_preflights.py +8 -0
  534. cumulusci/tasks/salesforce/network_member_group.py +178 -0
  535. cumulusci/tasks/salesforce/nonsourcetracking.py +228 -0
  536. cumulusci/tasks/salesforce/org_settings.py +193 -0
  537. cumulusci/tasks/salesforce/package_upload.py +328 -0
  538. cumulusci/tasks/salesforce/profiles.py +74 -0
  539. cumulusci/tasks/salesforce/promote_package_version.py +376 -0
  540. cumulusci/tasks/salesforce/retrieve_profile.py +195 -0
  541. cumulusci/tasks/salesforce/salesforce_files.py +244 -0
  542. cumulusci/tasks/salesforce/sourcetracking.py +507 -0
  543. cumulusci/tasks/salesforce/tests/__init__.py +3 -0
  544. cumulusci/tasks/salesforce/tests/test_CreateCommunity.py +278 -0
  545. cumulusci/tasks/salesforce/tests/test_CreatePackage.py +22 -0
  546. cumulusci/tasks/salesforce/tests/test_Deploy.py +470 -0
  547. cumulusci/tasks/salesforce/tests/test_DeployBundles.py +76 -0
  548. cumulusci/tasks/salesforce/tests/test_EnsureRecordTypes.py +345 -0
  549. cumulusci/tasks/salesforce/tests/test_ListCommunities.py +84 -0
  550. cumulusci/tasks/salesforce/tests/test_ListCommunityTemplates.py +49 -0
  551. cumulusci/tasks/salesforce/tests/test_PackageUpload.py +547 -0
  552. cumulusci/tasks/salesforce/tests/test_ProfileGrantAllAccess.py +699 -0
  553. cumulusci/tasks/salesforce/tests/test_PublishCommunity.py +181 -0
  554. cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py +24 -0
  555. cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py +56 -0
  556. cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py +21 -0
  557. cumulusci/tasks/salesforce/tests/test_SOQLQuery.py +30 -0
  558. cumulusci/tasks/salesforce/tests/test_UninstallLocal.py +15 -0
  559. cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py +19 -0
  560. cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py +22 -0
  561. cumulusci/tasks/salesforce/tests/test_UninstallPackage.py +19 -0
  562. cumulusci/tasks/salesforce/tests/test_UninstallPackaged.py +66 -0
  563. cumulusci/tasks/salesforce/tests/test_UninstallPackagedIncremental.py +127 -0
  564. cumulusci/tasks/salesforce/tests/test_activate_flow.py +132 -0
  565. cumulusci/tasks/salesforce/tests/test_base_tasks.py +110 -0
  566. cumulusci/tasks/salesforce/tests/test_check_components.py +445 -0
  567. cumulusci/tasks/salesforce/tests/test_composite.py +250 -0
  568. cumulusci/tasks/salesforce/tests/test_create_permission_sets.py +41 -0
  569. cumulusci/tasks/salesforce/tests/test_custom_settings.py +227 -0
  570. cumulusci/tasks/salesforce/tests/test_custom_settings_wait.py +174 -0
  571. cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py +18 -0
  572. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +240 -0
  573. cumulusci/tasks/salesforce/tests/test_insert_record.py +110 -0
  574. cumulusci/tasks/salesforce/tests/test_install_package_version.py +464 -0
  575. cumulusci/tasks/salesforce/tests/test_network_member_group.py +444 -0
  576. cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py +235 -0
  577. cumulusci/tasks/salesforce/tests/test_org_settings.py +407 -0
  578. cumulusci/tasks/salesforce/tests/test_profiles.py +202 -0
  579. cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +287 -0
  580. cumulusci/tasks/salesforce/tests/test_salesforce_files.py +228 -0
  581. cumulusci/tasks/salesforce/tests/test_sourcetracking.py +350 -0
  582. cumulusci/tasks/salesforce/tests/test_trigger_handlers.py +300 -0
  583. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +509 -0
  584. cumulusci/tasks/salesforce/tests/util.py +79 -0
  585. cumulusci/tasks/salesforce/trigger_handlers.py +119 -0
  586. cumulusci/tasks/salesforce/uninstall_packaged_incremental.py +136 -0
  587. cumulusci/tasks/salesforce/update_dependencies.py +290 -0
  588. cumulusci/tasks/salesforce/update_profile.py +339 -0
  589. cumulusci/tasks/salesforce/users/permsets.py +227 -0
  590. cumulusci/tasks/salesforce/users/photos.py +162 -0
  591. cumulusci/tasks/salesforce/users/tests/photo.mock.txt +1 -0
  592. cumulusci/tasks/salesforce/users/tests/test_permsets.py +950 -0
  593. cumulusci/tasks/salesforce/users/tests/test_photos.py +373 -0
  594. cumulusci/tasks/sample_data/capture_sample_data.py +77 -0
  595. cumulusci/tasks/sample_data/load_sample_data.py +85 -0
  596. cumulusci/tasks/sample_data/test_capture_sample_data.py +117 -0
  597. cumulusci/tasks/sample_data/test_load_sample_data.py +121 -0
  598. cumulusci/tasks/sfdx.py +83 -0
  599. cumulusci/tasks/tests/__init__.py +1 -0
  600. cumulusci/tasks/tests/conftest.py +30 -0
  601. cumulusci/tasks/tests/test_command.py +129 -0
  602. cumulusci/tasks/tests/test_connectedapp.py +236 -0
  603. cumulusci/tasks/tests/test_create_package_version.py +847 -0
  604. cumulusci/tasks/tests/test_datadictionary.py +1575 -0
  605. cumulusci/tasks/tests/test_dx_convert_from.py +60 -0
  606. cumulusci/tasks/tests/test_metadeploy.py +624 -0
  607. cumulusci/tasks/tests/test_metaxml.py +99 -0
  608. cumulusci/tasks/tests/test_promote_package_version.py +488 -0
  609. cumulusci/tasks/tests/test_pushfails.py +96 -0
  610. cumulusci/tasks/tests/test_salesforce.py +72 -0
  611. cumulusci/tasks/tests/test_sfdx.py +105 -0
  612. cumulusci/tasks/tests/test_util.py +207 -0
  613. cumulusci/tasks/util.py +261 -0
  614. cumulusci/tasks/vcs/__init__.py +19 -0
  615. cumulusci/tasks/vcs/commit_status.py +58 -0
  616. cumulusci/tasks/vcs/create_commit_status.py +37 -0
  617. cumulusci/tasks/vcs/download_extract.py +199 -0
  618. cumulusci/tasks/vcs/merge.py +298 -0
  619. cumulusci/tasks/vcs/publish.py +207 -0
  620. cumulusci/tasks/vcs/pull_request.py +9 -0
  621. cumulusci/tasks/vcs/release.py +134 -0
  622. cumulusci/tasks/vcs/release_report.py +105 -0
  623. cumulusci/tasks/vcs/tag.py +31 -0
  624. cumulusci/tasks/vcs/tests/github/test_commit_status.py +196 -0
  625. cumulusci/tasks/vcs/tests/github/test_download_extract.py +896 -0
  626. cumulusci/tasks/vcs/tests/github/test_merge.py +1118 -0
  627. cumulusci/tasks/vcs/tests/github/test_publish.py +823 -0
  628. cumulusci/tasks/vcs/tests/github/test_pull_request.py +29 -0
  629. cumulusci/tasks/vcs/tests/github/test_release.py +390 -0
  630. cumulusci/tasks/vcs/tests/github/test_release_report.py +109 -0
  631. cumulusci/tasks/vcs/tests/github/test_tag.py +90 -0
  632. cumulusci/tasks/vlocity/exceptions.py +2 -0
  633. cumulusci/tasks/vlocity/tests/test_vlocity.py +283 -0
  634. cumulusci/tasks/vlocity/vlocity.py +342 -0
  635. cumulusci/tests/__init__.py +1 -0
  636. cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml +18 -0
  637. cumulusci/tests/cassettes/TestIntegrationInfrastructure.test_integration_tests.yaml +19 -0
  638. cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py +307 -0
  639. cumulusci/tests/pytest_plugins/pytest_sf_vcr.py +275 -0
  640. cumulusci/tests/pytest_plugins/pytest_sf_vcr_serializer.py +160 -0
  641. cumulusci/tests/pytest_plugins/pytest_typeguard.py +5 -0
  642. cumulusci/tests/pytest_plugins/test_vcr_string_compressor.py +49 -0
  643. cumulusci/tests/pytest_plugins/vcr_string_compressor.py +97 -0
  644. cumulusci/tests/shared_cassettes/GET_sobjects_Account_describe.yaml +18 -0
  645. cumulusci/tests/shared_cassettes/GET_sobjects_Case_describe.yaml +18 -0
  646. cumulusci/tests/shared_cassettes/GET_sobjects_Contact_describe.yaml +4838 -0
  647. cumulusci/tests/shared_cassettes/GET_sobjects_Custom__c_describe.yaml +242 -0
  648. cumulusci/tests/shared_cassettes/GET_sobjects_Event_describe.yaml +19 -0
  649. cumulusci/tests/shared_cassettes/GET_sobjects_Global_describe.yaml +1338 -0
  650. cumulusci/tests/shared_cassettes/GET_sobjects_Lead_describe.yaml +18 -0
  651. cumulusci/tests/shared_cassettes/GET_sobjects_OpportunityContactRole_describe.yaml +34 -0
  652. cumulusci/tests/shared_cassettes/GET_sobjects_Opportunity_describe.yaml +1261 -0
  653. cumulusci/tests/shared_cassettes/GET_sobjects_Organization.yaml +49 -0
  654. cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfoList_xml.tpl +15 -0
  655. cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfo_xml.tpl +13 -0
  656. cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_insert_xml.tpl +24 -0
  657. cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_upsert_xml.tpl +25 -0
  658. cumulusci/tests/test_entry_points.py +20 -0
  659. cumulusci/tests/test_integration_infrastructure.py +131 -0
  660. cumulusci/tests/test_main.py +9 -0
  661. cumulusci/tests/test_schema.py +32 -0
  662. cumulusci/tests/test_utils.py +657 -0
  663. cumulusci/tests/test_vcr_serializer.py +134 -0
  664. cumulusci/tests/uncompressed_cassette.yaml +83 -0
  665. cumulusci/tests/util.py +344 -0
  666. cumulusci/utils/__init__.py +731 -0
  667. cumulusci/utils/classutils.py +9 -0
  668. cumulusci/utils/collections.py +32 -0
  669. cumulusci/utils/deprecation.py +11 -0
  670. cumulusci/utils/encryption.py +31 -0
  671. cumulusci/utils/fileutils.py +295 -0
  672. cumulusci/utils/git.py +142 -0
  673. cumulusci/utils/http/multi_request.py +214 -0
  674. cumulusci/utils/http/requests_utils.py +103 -0
  675. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +32 -0
  676. cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_composite_parallel_salesforce.yaml +65 -0
  677. cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_errors.yaml +24 -0
  678. cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_reference_ids.yaml +49 -0
  679. cumulusci/utils/http/tests/test_multi_request.py +255 -0
  680. cumulusci/utils/iterators.py +21 -0
  681. cumulusci/utils/logging.py +128 -0
  682. cumulusci/utils/metaprogramming.py +10 -0
  683. cumulusci/utils/options.py +138 -0
  684. cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py +29 -0
  685. cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py +50 -0
  686. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +238 -0
  687. cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py +243 -0
  688. cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py +353 -0
  689. cumulusci/utils/salesforce/count_sobjects.py +46 -0
  690. cumulusci/utils/salesforce/soql.py +17 -0
  691. cumulusci/utils/salesforce/tests/cassettes/ManualEdit_TestCountSObjects.test_count_sobjects__network_errors.yaml +23 -0
  692. cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects__errors.yaml +33 -0
  693. cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects_simple.yaml +29 -0
  694. cumulusci/utils/salesforce/tests/test_count_sobjects.py +29 -0
  695. cumulusci/utils/salesforce/tests/test_soql.py +30 -0
  696. cumulusci/utils/tests/cassettes/ManualEditTestDescribeOrg.test_minimal_schema.yaml +36 -0
  697. cumulusci/utils/tests/cassettes/ManualEdit_test_describe_to_sql.yaml +191 -0
  698. cumulusci/utils/tests/test_fileutils.py +284 -0
  699. cumulusci/utils/tests/test_git.py +85 -0
  700. cumulusci/utils/tests/test_logging.py +70 -0
  701. cumulusci/utils/tests/test_option_parsing.py +188 -0
  702. cumulusci/utils/tests/test_org_schema.py +691 -0
  703. cumulusci/utils/tests/test_org_schema_models.py +79 -0
  704. cumulusci/utils/tests/test_waiting.py +25 -0
  705. cumulusci/utils/version_strings.py +391 -0
  706. cumulusci/utils/waiting.py +42 -0
  707. cumulusci/utils/xml/__init__.py +91 -0
  708. cumulusci/utils/xml/metadata_tree.py +299 -0
  709. cumulusci/utils/xml/robot_xml.py +114 -0
  710. cumulusci/utils/xml/salesforce_encoding.py +100 -0
  711. cumulusci/utils/xml/test/test_metadata_tree.py +251 -0
  712. cumulusci/utils/xml/test/test_salesforce_encoding.py +173 -0
  713. cumulusci/utils/yaml/cumulusci_yml.py +401 -0
  714. cumulusci/utils/yaml/model_parser.py +156 -0
  715. cumulusci/utils/yaml/safer_loader.py +74 -0
  716. cumulusci/utils/yaml/tests/bad_cci.yml +5 -0
  717. cumulusci/utils/yaml/tests/cassettes/TestCumulusciYml.test_validate_url__with_errors.yaml +20 -0
  718. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +286 -0
  719. cumulusci/utils/yaml/tests/test_model_parser.py +175 -0
  720. cumulusci/utils/yaml/tests/test_safer_loader.py +88 -0
  721. cumulusci/utils/ziputils.py +61 -0
  722. cumulusci/vcs/base.py +143 -0
  723. cumulusci/vcs/bootstrap.py +272 -0
  724. cumulusci/vcs/github/__init__.py +24 -0
  725. cumulusci/vcs/github/adapter.py +689 -0
  726. cumulusci/vcs/github/release_notes/generator.py +219 -0
  727. cumulusci/vcs/github/release_notes/parser.py +151 -0
  728. cumulusci/vcs/github/release_notes/provider.py +143 -0
  729. cumulusci/vcs/github/service.py +569 -0
  730. cumulusci/vcs/github/tests/test_adapter.py +138 -0
  731. cumulusci/vcs/github/tests/test_service.py +408 -0
  732. cumulusci/vcs/models.py +586 -0
  733. cumulusci/vcs/tests/conftest.py +41 -0
  734. cumulusci/vcs/tests/dummy_service.py +241 -0
  735. cumulusci/vcs/tests/test_vcs_base.py +687 -0
  736. cumulusci/vcs/tests/test_vcs_bootstrap.py +727 -0
  737. cumulusci/vcs/utils/__init__.py +31 -0
  738. cumulusci/vcs/vcs_source.py +287 -0
  739. cumulusci_plus-5.0.0.dist-info/METADATA +145 -0
  740. cumulusci_plus-5.0.0.dist-info/RECORD +744 -0
  741. cumulusci_plus-5.0.0.dist-info/WHEEL +4 -0
  742. cumulusci_plus-5.0.0.dist-info/entry_points.txt +3 -0
  743. cumulusci_plus-5.0.0.dist-info/licenses/AUTHORS.rst +41 -0
  744. cumulusci_plus-5.0.0.dist-info/licenses/LICENSE +30 -0
@@ -0,0 +1,1196 @@
1
+ import tempfile
2
+ import typing as T
3
+ from collections import defaultdict
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+ from unittest.mock import MagicMock
7
+
8
+ from sqlalchemy import Column, MetaData, Table, Unicode, create_engine, func, inspect
9
+ from sqlalchemy.ext.automap import automap_base
10
+ from sqlalchemy.orm import Session
11
+
12
+ from cumulusci.core.enums import StrEnum
13
+ from cumulusci.core.exceptions import BulkDataException, TaskOptionsError
14
+ from cumulusci.core.utils import process_bool_arg
15
+ from cumulusci.salesforce_api.org_schema import get_org_schema
16
+ from cumulusci.tasks.bulkdata.dates import adjust_relative_dates
17
+ from cumulusci.tasks.bulkdata.mapping_parser import (
18
+ CaseInsensitiveDict,
19
+ MappingLookup,
20
+ MappingStep,
21
+ parse_from_yaml,
22
+ validate_and_inject_mapping,
23
+ )
24
+ from cumulusci.tasks.bulkdata.query_transformers import (
25
+ ID_TABLE_NAME,
26
+ AddLookupsToQuery,
27
+ AddMappingFiltersToQuery,
28
+ AddPersonAccountsToQuery,
29
+ AddRecordTypesToQuery,
30
+ DynamicLookupQueryExtender,
31
+ )
32
+ from cumulusci.tasks.bulkdata.step import (
33
+ DEFAULT_BULK_BATCH_SIZE,
34
+ DataApi,
35
+ DataOperationJobResult,
36
+ DataOperationStatus,
37
+ DataOperationType,
38
+ RestApiDmlOperation,
39
+ get_dml_operation,
40
+ )
41
+ from cumulusci.tasks.bulkdata.upsert_utils import (
42
+ AddUpsertsToQuery,
43
+ extract_upsert_key_data,
44
+ needs_etl_upsert,
45
+ )
46
+ from cumulusci.tasks.bulkdata.utils import (
47
+ RowErrorChecker,
48
+ SqlAlchemyMixin,
49
+ sql_bulk_insert_from_records,
50
+ )
51
+ from cumulusci.tasks.salesforce import BaseSalesforceApiTask
52
+
53
+
54
+ class LoadData(SqlAlchemyMixin, BaseSalesforceApiTask):
55
+ """Perform Bulk API operations to load data defined by a mapping from a local store into an org."""
56
+
57
+ task_options = {
58
+ "database_url": {
59
+ "description": "The database url to a database containing the test data to load"
60
+ },
61
+ "mapping": {
62
+ "description": "The path to a yaml file containing mappings of the database fields to Salesforce object fields",
63
+ "required": True,
64
+ },
65
+ "start_step": {
66
+ "description": "If specified, skip steps before this one in the mapping",
67
+ "required": False,
68
+ },
69
+ "sql_path": {
70
+ "description": "If specified, a database will be created from an SQL script at the provided path"
71
+ },
72
+ "ignore_row_errors": {
73
+ "description": "If True, allow the load to continue even if individual rows fail to load."
74
+ },
75
+ "reset_oids": {
76
+ "description": "If True (the default), and the _sf_ids tables exist, reset them before continuing.",
77
+ "required": False,
78
+ },
79
+ "bulk_mode": {
80
+ "description": "Set to Serial to force serial mode on all jobs. Parallel is the default."
81
+ },
82
+ "inject_namespaces": {
83
+ "description": "If True, the package namespace prefix will be "
84
+ "automatically added to (or removed from) objects "
85
+ "and fields based on the name used in the org. Defaults to True."
86
+ },
87
+ "drop_missing_schema": {
88
+ "description": "Set to True to skip any missing objects or fields instead of stopping with an error."
89
+ },
90
+ "set_recently_viewed": {
91
+ "description": "By default, the first 1000 records inserted via the Bulk API will be set as recently viewed. If fewer than 1000 records are inserted, existing objects of the same type being inserted will also be set as recently viewed.",
92
+ },
93
+ "org_shape_match_only": {
94
+ "description": "When True, all path options are ignored and only a dataset matching the org shape name will be loaded. Defaults to False."
95
+ },
96
+ "enable_rollback": {
97
+ "description": "When True, performs rollback operation incase of error. Defaults to False"
98
+ },
99
+ }
100
+ row_warning_limit = 10
101
+
102
+ def _init_options(self, kwargs):
103
+ super(LoadData, self)._init_options(kwargs)
104
+
105
+ self.options["ignore_row_errors"] = process_bool_arg(
106
+ self.options.get("ignore_row_errors") or False
107
+ )
108
+ self._init_dataset()
109
+ self.reset_oids = self.options.get("reset_oids", True)
110
+ self.bulk_mode = (
111
+ self.options.get("bulk_mode") and self.options.get("bulk_mode").title()
112
+ )
113
+ if self.bulk_mode and self.bulk_mode not in ["Serial", "Parallel"]:
114
+ raise TaskOptionsError("bulk_mode must be either Serial or Parallel")
115
+
116
+ inject_namespaces = self.options.get("inject_namespaces")
117
+ self.options["inject_namespaces"] = process_bool_arg(
118
+ True if inject_namespaces is None else inject_namespaces
119
+ )
120
+ self.options["drop_missing_schema"] = process_bool_arg(
121
+ self.options.get("drop_missing_schema") or False
122
+ )
123
+ self.options["set_recently_viewed"] = process_bool_arg(
124
+ self.options.get("set_recently_viewed", True)
125
+ )
126
+ self.options["enable_rollback"] = process_bool_arg(
127
+ self.options.get("enable_rollback", False)
128
+ )
129
+ self._id_generators = {}
130
+ self._old_format = False
131
+ self.ID_TABLE_NAME = ID_TABLE_NAME
132
+
133
+ def _init_dataset(self):
134
+ """Find the dataset paths to use with the following sequence:
135
+ 1. If `org_shape_match_only` is True (defaults False),
136
+ unset any other path options that may have been supplied.
137
+ 2. Prefer a supplied `database_url`.
138
+ 3. If `sql_path` was supplied, but not `mapping`, use default `mapping` value.
139
+ 4. If `mapping` was supplied, but not `sql_path`, use default `sql_path` value.
140
+ 5. If no path options were supplied, look for a dataset matching the org shape.
141
+ 6. If no matching dataset was found AND `org_shape_match_only` is False,
142
+ look for a dataset with the default `mapping` and `sql_path` values
143
+ (as previously defaulted in the standard library yml).
144
+ """
145
+ self.options["org_shape_match_only"] = process_bool_arg(
146
+ self.options.get("org_shape_match_only", False)
147
+ )
148
+ if self.options["org_shape_match_only"]:
149
+ self.options["mapping"] = None
150
+ self.options["sql_path"] = None
151
+ self.options["database_url"] = None
152
+ self.logger.warning(
153
+ "The `default_dataset_only` option has been deprecated. "
154
+ "Please switch to the `load_sample_data` task."
155
+ )
156
+
157
+ self.options.setdefault("database_url", None)
158
+ if self.options.get("database_url"):
159
+ # prefer database_url if it's set
160
+ self.options["sql_path"] = None
161
+ elif self.options.get("sql_path"):
162
+ self.options.setdefault("mapping", "datasets/mapping.yml")
163
+ elif self.options.get("mapping"):
164
+ self.options.setdefault("sql_path", "datasets/sample.sql")
165
+ elif found_dataset := (
166
+ self._find_matching_dataset() or self._find_default_dataset()
167
+ ): # didn't get either database_url or sql_path
168
+ mapping_path, dataset_path = found_dataset
169
+ self.options["mapping"] = mapping_path
170
+ self.options["sql_path"] = dataset_path
171
+ else:
172
+ self.has_dataset = False
173
+ return
174
+ self.has_dataset = True
175
+
176
+ def _find_matching_dataset(self) -> T.Optional[T.Tuple[str, str]]:
177
+ org_shape = self.org_config.lookup("config_name") if self.org_config else None
178
+ if not org_shape:
179
+ return None # persistent org
180
+ dataset_folder = f"datasets/{org_shape}"
181
+ if Path(dataset_folder).exists():
182
+ # check for dataset.sql and mapping.yml
183
+ mapping_path = f"{dataset_folder}/{org_shape}.mapping.yml"
184
+ dataset_path = f"{dataset_folder}/{org_shape}.dataset.sql"
185
+ if Path(mapping_path).exists() and Path(dataset_path).exists():
186
+ return (mapping_path, dataset_path)
187
+ else:
188
+ self.logger.warning(
189
+ f"Found datasets/{org_shape} but it did not contain {org_shape}.mapping.yml and {org_shape}.dataset.yml."
190
+ )
191
+ return None
192
+
193
+ def _find_default_dataset(self) -> T.Optional[T.Tuple[str, str]]:
194
+ if self.options["org_shape_match_only"]:
195
+ return None
196
+ dataset_path = "datasets/sample.sql"
197
+ mapping_path = "datasets/mapping.yml"
198
+ if Path(dataset_path).exists() and Path(mapping_path).exists():
199
+ return (mapping_path, dataset_path)
200
+ return None
201
+
202
+ def _run_task(self):
203
+ if not self.has_dataset:
204
+ if org_shape := self.org_config.lookup("config_name"):
205
+ self.logger.info(
206
+ f"No data will be loaded because there was no dataset found matching your org shape name ('{org_shape}')."
207
+ )
208
+ else:
209
+ self.logger.info(
210
+ "No data will be loaded because this is a persistent org and no dataset was specified."
211
+ )
212
+ return
213
+ self._init_mapping()
214
+ with self._init_db():
215
+ self._expand_mapping()
216
+ self._initialize_id_table(self.reset_oids)
217
+ start_step = self.options.get("start_step")
218
+ started = False
219
+ results = {}
220
+ for name, mapping in self.mapping.items():
221
+ # Skip steps until start_step
222
+ if not started and start_step and name != start_step:
223
+ self.logger.info(f"Skipping step: {name}")
224
+ continue
225
+
226
+ started = True
227
+
228
+ self.logger.info(f"Running step: {name}")
229
+ result = self._execute_step(mapping)
230
+ if result.status is DataOperationStatus.JOB_FAILURE:
231
+ raise BulkDataException(
232
+ f"Step {name} did not complete successfully: {','.join(result.job_errors)}"
233
+ )
234
+
235
+ if name in self.after_steps:
236
+ for after_name, after_step in self.after_steps[name].items():
237
+ self.logger.info(f"Running post-load step: {after_name}")
238
+ result = self._execute_step(after_step)
239
+ if result.status is DataOperationStatus.JOB_FAILURE:
240
+ raise BulkDataException(
241
+ f"Step {after_name} did not complete successfully: {','.join(result.job_errors)}"
242
+ )
243
+ results[name] = StepResultInfo(
244
+ mapping.sf_object, result, mapping.record_type
245
+ )
246
+ if self.options["set_recently_viewed"]:
247
+ try:
248
+ self.logger.info("Setting records to 'recently viewed'.")
249
+ set_recently_viewed = self._set_viewed()
250
+ except Exception as e:
251
+ self.logger.warning(f"Could not set recently viewed because {e}")
252
+ set_recently_viewed = [SetRecentlyViewedInfo("ALL", e)]
253
+ else:
254
+ set_recently_viewed = False
255
+
256
+ self.return_values = {
257
+ "step_results": {
258
+ step_name: result_info.simplify()
259
+ for step_name, result_info in results.items()
260
+ },
261
+ }
262
+ if set_recently_viewed is not False:
263
+ self.return_values["set_recently_viewed"] = set_recently_viewed
264
+
265
+ def _execute_step(
266
+ self, mapping: MappingStep
267
+ ) -> T.Union[DataOperationJobResult, MagicMock]:
268
+ """Load data for a single step."""
269
+
270
+ if "RecordTypeId" in mapping.fields:
271
+ conn = self.session.connection()
272
+ self._load_record_types([mapping.sf_object], conn)
273
+ self.session.commit()
274
+
275
+ step, query = self.configure_step(mapping)
276
+
277
+ with tempfile.TemporaryFile(mode="w+t") as local_ids:
278
+ # Store the previous values of the records before upsert
279
+ # This is so that we can perform rollback
280
+ if (
281
+ mapping.action
282
+ in [
283
+ DataOperationType.ETL_UPSERT,
284
+ DataOperationType.UPSERT,
285
+ DataOperationType.UPDATE,
286
+ ]
287
+ and self.options["enable_rollback"]
288
+ ):
289
+ UpdateRollback.prepare_for_rollback(
290
+ self, step, self._stream_queried_data(mapping, local_ids, query)
291
+ )
292
+ step.start()
293
+ if mapping.action == DataOperationType.SELECT:
294
+ step.select_records(
295
+ self._stream_queried_data(mapping, local_ids, query)
296
+ )
297
+ else:
298
+ step.load_records(self._stream_queried_data(mapping, local_ids, query))
299
+ step.end()
300
+
301
+ # Process Job Results
302
+ if step.job_result.status is not DataOperationStatus.JOB_FAILURE:
303
+ local_ids.seek(0)
304
+ self._process_job_results(mapping, step, local_ids)
305
+ elif (
306
+ step.job_result.status is DataOperationStatus.JOB_FAILURE
307
+ and self.options["enable_rollback"]
308
+ ):
309
+ Rollback._perform_rollback(self)
310
+
311
+ return step.job_result
312
+
313
+ def process_lookup_fields(self, mapping, fields, polymorphic_fields):
314
+ """Modify fields and priority fields based on lookup and polymorphic checks."""
315
+ # Store the lookups and their original order for re-insertion at the end
316
+ original_lookups = [name for name in fields if name in mapping.lookups]
317
+ max_insert_index = -1
318
+ for name, lookup in mapping.lookups.items():
319
+ if name in fields:
320
+ # Get the index of the lookup field before removing it
321
+ insert_index = fields.index(name)
322
+ max_insert_index = max(max_insert_index, insert_index)
323
+ # Remove the lookup field from fields
324
+ fields.remove(name)
325
+
326
+ # Do the same for priority fields
327
+ lookup_in_priority_fields = False
328
+ if name in mapping.select_options.priority_fields:
329
+ # Set flag to True
330
+ lookup_in_priority_fields = True
331
+ # Remove the lookup field from priority fields
332
+ del mapping.select_options.priority_fields[name]
333
+
334
+ # Check if this lookup field is polymorphic
335
+ if (
336
+ name in polymorphic_fields
337
+ and len(polymorphic_fields[name]["referenceTo"]) > 1
338
+ ):
339
+ # Convert to list if string
340
+ if not isinstance(lookup.table, list):
341
+ lookup.table = [lookup.table]
342
+ # Polymorphic field handling
343
+ polymorphic_references = lookup.table
344
+ relationship_name = polymorphic_fields[name]["relationshipName"]
345
+
346
+ # Loop through each polymorphic type (e.g., Contact, Lead)
347
+ for ref_type in polymorphic_references:
348
+ # Find the mapping step for this polymorphic type
349
+ lookup_mapping_step = next(
350
+ (
351
+ step
352
+ for step in self.mapping.values()
353
+ if step.table == ref_type
354
+ ),
355
+ None,
356
+ )
357
+ if lookup_mapping_step:
358
+ lookup_fields = lookup_mapping_step.fields.keys()
359
+ # Insert fields in the format {relationship_name}.{ref_type}.{lookup_field}
360
+ for field in lookup_fields:
361
+ fields.insert(
362
+ insert_index,
363
+ f"{relationship_name}.{lookup_mapping_step.sf_object}.{field}",
364
+ )
365
+ insert_index += 1
366
+ max_insert_index = max(max_insert_index, insert_index)
367
+ if lookup_in_priority_fields:
368
+ mapping.select_options.priority_fields[
369
+ f"{relationship_name}.{lookup_mapping_step.sf_object}.{field}"
370
+ ] = f"{relationship_name}.{lookup_mapping_step.sf_object}.{field}"
371
+
372
+ else:
373
+ # Non-polymorphic field handling
374
+ lookup_table = lookup.table
375
+
376
+ if isinstance(lookup_table, list):
377
+ lookup_table = lookup_table[0]
378
+
379
+ # Get the mapping step for the non-polymorphic reference
380
+ lookup_mapping_step = next(
381
+ (
382
+ step
383
+ for step in self.mapping.values()
384
+ if step.table == lookup_table
385
+ ),
386
+ None,
387
+ )
388
+
389
+ if lookup_mapping_step:
390
+ relationship_name = polymorphic_fields[name]["relationshipName"]
391
+ lookup_fields = lookup_mapping_step.fields.keys()
392
+
393
+ # Insert the new fields at the same position as the removed lookup field
394
+ for field in lookup_fields:
395
+ fields.insert(insert_index, f"{relationship_name}.{field}")
396
+ insert_index += 1
397
+ max_insert_index = max(max_insert_index, insert_index)
398
+ if lookup_in_priority_fields:
399
+ mapping.select_options.priority_fields[
400
+ f"{relationship_name}.{field}"
401
+ ] = f"{relationship_name}.{field}"
402
+
403
+ # Append the original lookups at the end in the same order
404
+ for name in original_lookups:
405
+ if name not in fields:
406
+ fields.insert(max_insert_index, name)
407
+ max_insert_index += 1
408
+
409
+ def configure_step(self, mapping):
410
+ """Create a step appropriate to the action"""
411
+ bulk_mode = mapping.bulk_mode or self.bulk_mode or "Parallel"
412
+ api_options = {"batch_size": mapping.batch_size, "bulk_mode": bulk_mode}
413
+ num_records_in_target = None
414
+ content_type = None
415
+
416
+ fields = mapping.get_load_field_list()
417
+
418
+ # implement "smart" upsert
419
+ if mapping.action == DataOperationType.SMART_UPSERT:
420
+ if needs_etl_upsert(mapping, self.sf):
421
+ mapping.action = DataOperationType.ETL_UPSERT
422
+ else:
423
+ mapping.action = DataOperationType.UPSERT
424
+
425
+ if mapping.action == DataOperationType.ETL_UPSERT:
426
+ extract_upsert_key_data(
427
+ mapping.sf_object,
428
+ mapping.update_key,
429
+ self,
430
+ self.metadata,
431
+ self.session.connection(),
432
+ )
433
+
434
+ # If we treat "Id" as an "external_id_name" then it's
435
+ # allowed to be sparse.
436
+ api_options["update_key"] = "Id"
437
+ action = DataOperationType.UPSERT
438
+ fields.append("Id")
439
+ elif mapping.action == DataOperationType.UPSERT:
440
+ self.check_simple_upsert(mapping)
441
+ api_options["update_key"] = mapping.update_key[0]
442
+ action = DataOperationType.UPSERT
443
+ elif mapping.action == DataOperationType.SELECT:
444
+ # Set content type to json
445
+ content_type = "JSON"
446
+ # Bulk process expects DataOpertionType to be QUERY
447
+ action = DataOperationType.QUERY
448
+ # Determine number of records in the target org
449
+ record_count_response = self.sf.restful(
450
+ f"limits/recordCount?sObjects={mapping.sf_object}"
451
+ )
452
+ sobject_map = {
453
+ entry["name"]: entry["count"]
454
+ for entry in record_count_response["sObjects"]
455
+ }
456
+ num_records_in_target = sobject_map.get(mapping.sf_object, None)
457
+
458
+ # Check for similarity selection strategy and modify fields accordingly
459
+ if mapping.select_options.strategy == "similarity":
460
+ # Describe the object to determine polymorphic lookups
461
+ describe_result = self.sf.restful(
462
+ f"sobjects/{mapping.sf_object}/describe"
463
+ )
464
+ polymorphic_fields = {
465
+ field["name"]: field
466
+ for field in describe_result["fields"]
467
+ if field["type"] == "reference"
468
+ }
469
+ self.process_lookup_fields(mapping, fields, polymorphic_fields)
470
+ else:
471
+ action = mapping.action
472
+
473
+ query = self._query_db(mapping)
474
+
475
+ # Set volume
476
+ volume = (
477
+ num_records_in_target
478
+ if num_records_in_target is not None
479
+ else query.count()
480
+ )
481
+
482
+ step = get_dml_operation(
483
+ sobject=mapping.sf_object,
484
+ operation=action,
485
+ api_options=api_options,
486
+ context=self,
487
+ fields=fields,
488
+ api=mapping.api,
489
+ volume=volume,
490
+ selection_strategy=mapping.select_options.strategy,
491
+ selection_filter=mapping.select_options.filter,
492
+ selection_priority_fields=mapping.select_options.priority_fields,
493
+ content_type=content_type,
494
+ threshold=mapping.select_options.threshold,
495
+ )
496
+ return step, query
497
+
498
+ def check_simple_upsert(self, mapping):
499
+ """Check that this upsert is correct."""
500
+ if needs_etl_upsert(mapping, self.sf):
501
+ raise BulkDataException(
502
+ f"This update key is not compatible with a simple upsert: `{','.join(mapping.update_key)}`. "
503
+ "Use `action: ETL_UPSERT` instead."
504
+ )
505
+
506
+ def _stream_queried_data(self, mapping, local_ids, query):
507
+ """Get data from the local db"""
508
+
509
+ statics = self._get_statics(mapping)
510
+ total_rows = 0
511
+
512
+ if mapping.anchor_date:
513
+ date_context = mapping.get_relative_date_context(
514
+ mapping.get_load_field_list(), self.sf
515
+ )
516
+ # Clamping the yield from the query ensures we do not
517
+ # create more Bulk API batches than expected, regardless
518
+ # of batch size, while capping memory usage.
519
+ batch_size = mapping.batch_size or DEFAULT_BULK_BATCH_SIZE
520
+ for row in query.yield_per(batch_size):
521
+ total_rows += 1
522
+ pkey = row[0]
523
+ row = list(row[1:]) + statics
524
+
525
+ if mapping.anchor_date and (date_context[0] or date_context[1]):
526
+ row = adjust_relative_dates(
527
+ mapping, date_context, row, DataOperationType.INSERT
528
+ )
529
+
530
+ if mapping.action is DataOperationType.UPDATE:
531
+ if len(row) > 1 and all([f is None for f in row[1:]]):
532
+ # Skip update rows that contain no values
533
+ total_rows -= 1
534
+ continue
535
+
536
+ local_ids.write(str(pkey) + "\n")
537
+ yield row
538
+
539
+ self.logger.info(
540
+ f"Prepared {total_rows} rows for {mapping.action.value} to {mapping.sf_object}."
541
+ )
542
+
543
+ def _load_record_types(self, sobjects, conn):
544
+ """Persist record types for the given sObjects into the database."""
545
+ for sobject in sobjects:
546
+ table_name = sobject + "_rt_target_mapping"
547
+ self._extract_record_types(
548
+ sobject, table_name, conn, self.org_config.is_person_accounts_enabled
549
+ )
550
+
551
+ def _get_statics(self, mapping):
552
+ """Return the static values (not column names) to be appended to
553
+ records for this mapping."""
554
+ statics = list(mapping.static.values())
555
+ if mapping.record_type:
556
+ query = (
557
+ f"SELECT Id FROM RecordType WHERE SObjectType='{mapping.sf_object}'"
558
+ f"AND DeveloperName = '{mapping.record_type}' LIMIT 1"
559
+ )
560
+ records = self.sf.query(query)["records"]
561
+ if records:
562
+ record_type_id = records[0]["Id"]
563
+ else:
564
+ raise BulkDataException(f"Cannot find RecordType with query `{query}`")
565
+ statics.append(record_type_id)
566
+
567
+ return statics
568
+
569
+ def _query_db(self, mapping):
570
+ """Build a query to retrieve data from the local db.
571
+
572
+ Includes columns from the mapping
573
+ as well as joining to the id tables to get real SF ids
574
+ for lookups.
575
+ """
576
+ model = self.models[mapping.table]
577
+
578
+ id_column = model.__table__.primary_key.columns.keys()[0]
579
+ columns = [getattr(model, id_column)]
580
+
581
+ table_cases = CaseInsensitiveDict(model.__table__.columns)
582
+ for name, f in mapping.fields.items():
583
+ if name not in ("Id", "RecordTypeId", "RecordType"):
584
+ column = table_cases.get(f)
585
+ columns.append(column)
586
+
587
+ query = self.session.query(*columns)
588
+
589
+ classes = [
590
+ AddRecordTypesToQuery,
591
+ AddMappingFiltersToQuery,
592
+ AddUpsertsToQuery,
593
+ ]
594
+ transformers = []
595
+ if (
596
+ mapping.action == DataOperationType.SELECT
597
+ and mapping.select_options.strategy == "similarity"
598
+ ):
599
+ transformers.append(
600
+ DynamicLookupQueryExtender(
601
+ mapping, self.mapping, self.metadata, model, self._old_format
602
+ )
603
+ )
604
+ transformers.append(
605
+ AddLookupsToQuery(mapping, self.metadata, model, self._old_format)
606
+ )
607
+
608
+ transformers.extend([cls(mapping, self.metadata, model) for cls in classes])
609
+
610
+ if mapping.sf_object == "Contact" and self._can_load_person_accounts(mapping):
611
+ transformers.append(AddPersonAccountsToQuery(mapping, self.metadata, model))
612
+
613
+ for transformer in transformers:
614
+ query = transformer.add_columns(query)
615
+
616
+ for transformer in transformers:
617
+ query = transformer.add_filters(query)
618
+
619
+ for transformer in transformers:
620
+ query = transformer.add_outerjoins(query)
621
+
622
+ query = self._sort_by_lookups(query, mapping, model)
623
+ return query
624
+
625
+ def _sort_by_lookups(self, query, mapping, model):
626
+ lookups = [lookup for lookup in mapping.lookups.values() if not lookup.after]
627
+ for lookup in lookups:
628
+ key_field = lookup.get_lookup_key_field(model)
629
+ lookup_column = getattr(model, key_field)
630
+ query = query.order_by(lookup_column)
631
+
632
+ return query
633
+
634
+ def _process_job_results(self, mapping, step, local_ids):
635
+ """Get the job results and process the results. If we're raising for
636
+ row-level errors, do so; if we're inserting, store the new Ids."""
637
+
638
+ is_insert_upsert_or_select = mapping.action in (
639
+ DataOperationType.INSERT,
640
+ DataOperationType.UPSERT,
641
+ DataOperationType.ETL_UPSERT,
642
+ DataOperationType.SELECT,
643
+ )
644
+
645
+ conn = self.session.connection()
646
+ sf_id_results = self._generate_results_id_map(step, local_ids)
647
+
648
+ for i in range(len(sf_id_results)):
649
+ # Check for old_format of load sql files
650
+ if str(sf_id_results[i][0]).isnumeric():
651
+ self._old_format = True
652
+ # Set id column with new naming format (<sobject> - <counter>)
653
+ sf_id_results[i][0] = mapping.table + "-" + str(sf_id_results[i][0])
654
+ else:
655
+ break
656
+ # If we know we have no successful inserts, don't attempt to persist Ids.
657
+ # Do, however, drain the generator to get error-checking behavior.
658
+ if is_insert_upsert_or_select and (
659
+ step.job_result.records_processed - step.job_result.total_row_errors
660
+ ):
661
+ table = self.metadata.tables[self.ID_TABLE_NAME]
662
+ sql_bulk_insert_from_records(
663
+ connection=conn,
664
+ table=table,
665
+ columns=("id", "sf_id"),
666
+ record_iterable=sf_id_results,
667
+ )
668
+
669
+ # Contact records for Person Accounts are inserted during an Account
670
+ # sf_object step. Insert records into the Contact ID table for
671
+ # person account Contact records so lookups to
672
+ # person account Contact records get populated downstream as expected.
673
+ if (
674
+ is_insert_upsert_or_select
675
+ and mapping.sf_object == "Contact"
676
+ and self._can_load_person_accounts(mapping)
677
+ ):
678
+ account_id_lookup = mapping.lookups.get("AccountId")
679
+ if account_id_lookup:
680
+ sql_bulk_insert_from_records(
681
+ connection=conn,
682
+ table=self.metadata.tables[self.ID_TABLE_NAME],
683
+ columns=("id", "sf_id"),
684
+ record_iterable=self._generate_contact_id_map_for_person_accounts(
685
+ mapping, account_id_lookup, conn
686
+ ),
687
+ )
688
+
689
+ if is_insert_upsert_or_select:
690
+ self.session.commit()
691
+
692
+ def _generate_results_id_map(self, step, local_ids):
693
+ """Consume results from load and prepare rows for id table.
694
+ Raise BulkDataException on row errors if configured to do so.
695
+ Adds created records into insert_rollback Table
696
+ Performs rollback in case of any errors if enable_rollback is True"""
697
+ error_checker = RowErrorChecker(
698
+ self.logger, self.options["ignore_row_errors"], self.row_warning_limit
699
+ )
700
+ local_ids = (lid.strip("\n") for lid in local_ids)
701
+ sf_id_results = []
702
+ created_results = []
703
+ failed_results = []
704
+ for result, local_id in zip(step.get_results(), local_ids):
705
+ if result.success:
706
+ sf_id_results.append([local_id, result.id])
707
+ if result.created:
708
+ created_results.append([result.id])
709
+ else:
710
+ failed_results.append([result, local_id])
711
+
712
+ # We record failed_results separately since if a unsuccesful record
713
+ # was in between, it would not store all the successful ids
714
+ for result, local_id in failed_results:
715
+ try:
716
+ error_checker.check_for_row_error(result, local_id)
717
+ except Exception as e:
718
+ if self.options["enable_rollback"]:
719
+ CreateRollback.prepare_for_rollback(self, step, created_results)
720
+ Rollback._perform_rollback(self)
721
+ raise e
722
+ if self.options["enable_rollback"]:
723
+ CreateRollback.prepare_for_rollback(self, step, created_results)
724
+ return sf_id_results
725
+
726
+ def _initialize_id_table(self, should_reset_table):
727
+ """initalize or find table to hold the inserted SF Ids
728
+
729
+ The table has a name like xxx_sf_ids and has just two columns, id and sf_id.
730
+
731
+ If the table already exists, should_reset_table determines whether to
732
+ drop and recreate it or not.
733
+ """
734
+
735
+ already_exists = self.ID_TABLE_NAME in self.metadata.tables
736
+
737
+ if already_exists and not should_reset_table:
738
+ return
739
+ elif already_exists:
740
+ self.metadata.remove(self.metadata.tables[self.ID_TABLE_NAME])
741
+ id_table = Table(
742
+ self.ID_TABLE_NAME,
743
+ self.metadata,
744
+ Column("id", Unicode(255), primary_key=True),
745
+ Column("sf_id", Unicode(18)),
746
+ )
747
+ if id_table.exists():
748
+ id_table.drop()
749
+ id_table.create()
750
+
751
+ def _sqlite_load(self):
752
+ """Read a SQLite script and initialize the in-memory database."""
753
+ conn = self.session.connection()
754
+ cursor = conn.connection.cursor()
755
+ with open(self.options["sql_path"], "r", encoding="utf-8") as f:
756
+ try:
757
+ cursor.executescript(f.read())
758
+ finally:
759
+ cursor.close()
760
+ # self.session.flush()
761
+
762
+ @contextmanager
763
+ def _init_db(self):
764
+ """Initialize the database and automapper."""
765
+ # initialize the DB engine
766
+ with self._database_url() as database_url:
767
+ parent_engine = create_engine(database_url)
768
+ with parent_engine.connect() as connection:
769
+ # initialize the DB session
770
+ self.session = Session(connection)
771
+
772
+ if self.options.get("sql_path"):
773
+ self._sqlite_load()
774
+
775
+ # initialize DB metadata
776
+ self.metadata = MetaData()
777
+ self.metadata.bind = connection
778
+ self.inspector = inspect(parent_engine)
779
+
780
+ # empty the record of initalized tables
781
+ Rollback._initialized_rollback_tables_api = {}
782
+
783
+ # initialize the automap mapping
784
+ self.base = automap_base(bind=connection, metadata=self.metadata)
785
+ self.base.prepare(connection, reflect=True)
786
+
787
+ # Loop through mappings and reflect each referenced table
788
+ self.models = {}
789
+ for name, mapping in self.mapping.items():
790
+ if mapping.table not in self.models:
791
+ try:
792
+ self.models[mapping.table] = self.base.classes[
793
+ mapping.table
794
+ ]
795
+ except KeyError as e:
796
+ raise BulkDataException(f"Table not found in dataset: {e}")
797
+
798
+ # create any Record Type tables we need
799
+ if "RecordTypeId" in mapping.fields:
800
+ self._create_record_type_table(
801
+ mapping.get_destination_record_type_table()
802
+ )
803
+ self.metadata.create_all()
804
+
805
+ self._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
806
+ yield
807
+
808
+ def _init_mapping(self):
809
+ """Load a YAML mapping file."""
810
+ mapping_file_path = self.options.get("mapping")
811
+ if not mapping_file_path:
812
+ raise TaskOptionsError("Mapping file path required")
813
+
814
+ self.mapping = parse_from_yaml(mapping_file_path)
815
+
816
+ validate_and_inject_mapping(
817
+ mapping=self.mapping,
818
+ sf=self.sf,
819
+ namespace=self.project_config.project__package__namespace,
820
+ data_operation=DataOperationType.INSERT,
821
+ inject_namespaces=self.options["inject_namespaces"],
822
+ drop_missing=self.options["drop_missing_schema"],
823
+ )
824
+
825
+ def _expand_mapping(self):
826
+ """Walk the mapping and generate any required 'after' steps
827
+ to handle dependent and self-lookups."""
828
+ # Expand the mapping to handle dependent lookups
829
+ self.after_steps = defaultdict(dict)
830
+
831
+ for step in self.mapping.values():
832
+ if any([lookup.after for lookup in step.lookups.values()]):
833
+ # We have deferred/dependent lookups.
834
+ # Synthesize mapping steps for them.
835
+
836
+ sobject = step.sf_object
837
+ after_list = {
838
+ lookup.after for lookup in step.lookups.values() if lookup.after
839
+ }
840
+
841
+ for after in after_list:
842
+ lookups = {
843
+ lookup_field: lookup
844
+ for lookup_field, lookup in step.lookups.items()
845
+ if lookup.after == after
846
+ }
847
+ name = f"Update {sobject} Dependencies After {after}"
848
+ mapping = MappingStep(
849
+ sf_object=sobject,
850
+ api=step.api,
851
+ action="update",
852
+ table=step.table,
853
+ )
854
+ mapping.lookups["Id"] = MappingLookup(
855
+ name="Id",
856
+ table=step["table"],
857
+ key_field=self.models[
858
+ step["table"]
859
+ ].__table__.primary_key.columns.keys()[0],
860
+ )
861
+ for lookup in lookups:
862
+ mapping.lookups[lookup] = lookups[lookup].copy()
863
+ mapping.lookups[lookup].after = None
864
+
865
+ self.after_steps[after][name] = mapping
866
+
867
+ def _validate_org_has_person_accounts_enabled_if_person_account_data_exists(self):
868
+ """
869
+ To ensure data is loaded from the dataset as expected as well as avoid partial
870
+ failues, raise a BulkDataException if there exists Account or Contact records with
871
+ IsPersonAccount as 'true' but the org does not have person accounts enabled.
872
+ """
873
+ for mapping in self.mapping.values():
874
+ if mapping.sf_object in [
875
+ "Account",
876
+ "Contact",
877
+ ] and self._db_has_person_accounts_column(mapping):
878
+ table = self.models[mapping.table].__table__
879
+ if (
880
+ self.session.query(table)
881
+ .filter(table.columns.get("IsPersonAccount") == "true")
882
+ .first()
883
+ and not self.org_config.is_person_accounts_enabled
884
+ ):
885
+ raise BulkDataException(
886
+ "Your dataset contains Person Account data but Person Accounts is not enabled for your org."
887
+ )
888
+
889
+ def _db_has_person_accounts_column(self, mapping):
890
+ """Returns whether "IsPersonAccount" is a column in mapping's table."""
891
+ return (
892
+ self.models[mapping.table].__table__.columns.get("IsPersonAccount")
893
+ is not None
894
+ )
895
+
896
+ def _can_load_person_accounts(self, mapping) -> bool:
897
+ """Returns whether person accounts can be loaded:
898
+ - The mapping has a "IsPersonAccount" column
899
+ - Person Accounts is enabled in the org.
900
+ """
901
+ return (
902
+ self._db_has_person_accounts_column(mapping)
903
+ and self.org_config.is_person_accounts_enabled
904
+ )
905
+
906
+ def _generate_contact_id_map_for_person_accounts(
907
+ self, contact_mapping, account_id_lookup, conn
908
+ ):
909
+ """
910
+ Yields (local_id, sf_id) for Contact records where IsPersonAccount
911
+ is true that can handle large data volumes.
912
+
913
+ We know a Person Account record is related to one and only one Contact
914
+ record. Therefore, we can map local Contact IDs to Salesforce IDs
915
+ by previously inserted Account records:
916
+ - Query the DB to get the map: Salesforce Account ID ->
917
+ local Contact ID
918
+ - Query Salesforce to get the map: Salesforce Account ID ->
919
+ Salesforce Contact ID
920
+ - Merge the maps
921
+ """
922
+ # Contact table columns
923
+ contact_model = self.models[contact_mapping.table]
924
+
925
+ contact_id_column = getattr(
926
+ contact_model, contact_model.__table__.primary_key.columns.keys()[0]
927
+ )
928
+ account_id_column = getattr(
929
+ contact_model, account_id_lookup.get_lookup_key_field(contact_model)
930
+ )
931
+
932
+ # Account ID table + column
933
+ account_sf_ids_table = account_id_lookup.aliased_table
934
+ account_sf_id_column = account_sf_ids_table.columns["sf_id"]
935
+
936
+ # Query the Contact table for person account contact records so we can
937
+ # create a Map: Account SF ID --> Contact ID. Outer join the
938
+ # Account SF IDs table to get each Contact's associated
939
+ # Account SF ID.
940
+ if self._old_format:
941
+ query = (
942
+ self.session.query(contact_id_column, account_sf_id_column)
943
+ .filter(
944
+ func.lower(contact_model.__table__.columns.get("IsPersonAccount"))
945
+ == "true"
946
+ )
947
+ .outerjoin(
948
+ account_sf_ids_table,
949
+ account_sf_ids_table.columns["id"]
950
+ == str(account_id_lookup.table) + "-" + account_id_column,
951
+ )
952
+ )
953
+ else:
954
+ query = (
955
+ self.session.query(contact_id_column, account_sf_id_column)
956
+ .filter(
957
+ func.lower(contact_model.__table__.columns.get("IsPersonAccount"))
958
+ == "true"
959
+ )
960
+ .outerjoin(
961
+ account_sf_ids_table,
962
+ account_sf_ids_table.columns["id"] == account_id_column,
963
+ )
964
+ )
965
+
966
+ # Stream the results so we can process batches of 200 Contacts
967
+ # in case we have large data volumes.
968
+ query_result = conn.execution_options(stream_results=True).execute(
969
+ query.statement
970
+ )
971
+
972
+ while True:
973
+ # While we have a chunk to process
974
+ chunk = query_result.fetchmany(200)
975
+ if not chunk:
976
+ break
977
+
978
+ # Collect Map: Account SF ID --> Contact ID
979
+ contact_ids_by_account_sf_id = {record[1]: record[0] for record in chunk}
980
+
981
+ # Query Map: Account SF ID --> Contact SF ID
982
+ # It's safe to use query_all since the chunk size to 200.
983
+ for record in self.sf.query_all(
984
+ "SELECT Id, AccountId FROM Contact WHERE IsPersonAccount = true AND AccountId IN ('{}')".format(
985
+ "','".join(contact_ids_by_account_sf_id.keys())
986
+ )
987
+ )["records"]:
988
+ contact_id = contact_ids_by_account_sf_id.get(record["AccountId"])
989
+ contact_sf_id = record["Id"]
990
+
991
+ # Join maps together to get tuple (Contact ID, Contact SF ID) to insert into step's ID Table.
992
+ if self._old_format:
993
+ yield (contact_mapping.table + "-" + str(contact_id), contact_sf_id)
994
+ else:
995
+ yield (contact_id, contact_sf_id)
996
+
997
+ def _set_viewed(self) -> T.List["SetRecentlyViewedInfo"]:
998
+ """Set items as recently viewed. Filter out custom objects without custom tabs."""
999
+ object_names = set()
1000
+ custom_objects = set()
1001
+ results = []
1002
+
1003
+ # Separate standard and custom objects
1004
+ for mapping in self.mapping.values():
1005
+ object_name = mapping.sf_object
1006
+ if object_name.endswith("__c"):
1007
+ custom_objects.add(object_name)
1008
+ else:
1009
+ object_names.add(object_name)
1010
+ # collect SobjectName that have custom tabs
1011
+ if custom_objects:
1012
+ try:
1013
+ custom_tab_objects = self.sf.query_all(
1014
+ "SELECT SObjectName FROM TabDefinition WHERE IsCustom = true AND SObjectName IN ('{}')".format(
1015
+ "','".join(sorted(custom_objects))
1016
+ )
1017
+ )
1018
+ for record in custom_tab_objects["records"]:
1019
+ object_names.add(record["SobjectName"])
1020
+ except Exception as e:
1021
+ self.logger.warning(
1022
+ f"Cannot get the list of custom tabs to set recently viewed status on them. Error: {e}"
1023
+ )
1024
+ with get_org_schema(
1025
+ self.sf, self.org_config, included_objects=object_names, force_recache=True
1026
+ ) as org_schema:
1027
+ for mapped_item in sorted(object_names):
1028
+ if org_schema[mapped_item].mruEnabled:
1029
+ try:
1030
+ self.sf.query_all(
1031
+ f"SELECT Id FROM {mapped_item} ORDER BY CreatedDate DESC LIMIT 1000 FOR VIEW"
1032
+ )
1033
+ results.append(SetRecentlyViewedInfo(mapped_item, None))
1034
+ except Exception as e:
1035
+ self.logger.warning(
1036
+ f"Cannot set recently viewed status for {mapped_item}. Error: {e}"
1037
+ )
1038
+ results.append(SetRecentlyViewedInfo(mapped_item, e))
1039
+ return results
1040
+
1041
+
1042
+ class RollbackType(StrEnum):
1043
+ """Enum to specify type of rollback"""
1044
+
1045
+ UPSERT = "upsert_rollback"
1046
+ INSERT = "insert_rollback"
1047
+
1048
+
1049
+ class Rollback:
1050
+ # Store the table name and it's corresponding API (rest or bulk)
1051
+ _initialized_rollback_tables_api = {}
1052
+
1053
+ @staticmethod
1054
+ def _create_tables_for_rollback(context, step, rollback_type: RollbackType) -> str:
1055
+ """Create the tables required for upsert and insert rollback"""
1056
+ table_name = f"{step.sobject}_{rollback_type}"
1057
+
1058
+ if table_name not in Rollback._initialized_rollback_tables_api:
1059
+ common_columns = [Column("Id", Unicode(255), primary_key=True)]
1060
+
1061
+ additional_columns = (
1062
+ [Column(field, Unicode(255)) for field in step.fields if field != "Id"]
1063
+ if rollback_type is RollbackType.UPSERT
1064
+ else []
1065
+ )
1066
+
1067
+ columns = common_columns + additional_columns
1068
+
1069
+ # Create the table
1070
+ rollback_table = Table(table_name, context.metadata, *columns)
1071
+ rollback_table.create()
1072
+
1073
+ # Store the API in the initialized tables dictionary
1074
+ if isinstance(step, RestApiDmlOperation):
1075
+ Rollback._initialized_rollback_tables_api[table_name] = DataApi.REST
1076
+ else:
1077
+ Rollback._initialized_rollback_tables_api[table_name] = DataApi.BULK
1078
+
1079
+ return table_name
1080
+
1081
+ @staticmethod
1082
+ def _perform_rollback(context):
1083
+ """Perform total rollback"""
1084
+ context.logger.info("--Initiated Rollback Procedure--")
1085
+ for table in reversed(context.metadata.sorted_tables):
1086
+ if table.name.endswith(RollbackType.INSERT):
1087
+ CreateRollback._perform_rollback(context, table)
1088
+ elif table.name.endswith(RollbackType.UPSERT):
1089
+ UpdateRollback._perform_rollback(context, table)
1090
+ context.logger.info("--Finished Rollback Procedure--")
1091
+
1092
+
1093
+ class UpdateRollback:
1094
+ @staticmethod
1095
+ def prepare_for_rollback(context, step, records):
1096
+ """Retrieve previous values for records being updated"""
1097
+ results, columns = step.get_prev_record_values(records)
1098
+ if results:
1099
+ table_name = Rollback._create_tables_for_rollback(
1100
+ context, step, RollbackType.UPSERT
1101
+ )
1102
+ conn = context.session.connection()
1103
+ sql_bulk_insert_from_records(
1104
+ connection=conn,
1105
+ table=context.metadata.tables[table_name],
1106
+ columns=columns,
1107
+ record_iterable=results,
1108
+ )
1109
+
1110
+ @staticmethod
1111
+ def _perform_rollback(context, table: Table) -> None:
1112
+ """Perform rollback for updated records"""
1113
+ sf_object = table.name.split(f"_{RollbackType.UPSERT.value}")[0]
1114
+ records = context.session.query(table).all()
1115
+
1116
+ if records:
1117
+ context.logger.info(f"Reverting upserts for {sf_object}")
1118
+ api_options = {"update_key": "Id"}
1119
+
1120
+ # Use get_dml_operation to create an UPSERT step
1121
+ step = get_dml_operation(
1122
+ sobject=sf_object,
1123
+ operation=DataOperationType.UPSERT,
1124
+ api_options=api_options,
1125
+ context=context,
1126
+ fields=[column.name for column in table.columns],
1127
+ api=Rollback._initialized_rollback_tables_api[table.name],
1128
+ volume=len(records),
1129
+ )
1130
+ step.start()
1131
+ step.load_records(records)
1132
+ step.end()
1133
+ context.logger.info("Done")
1134
+
1135
+
1136
+ class CreateRollback:
1137
+ @staticmethod
1138
+ def prepare_for_rollback(context, step, records):
1139
+ """Store the sf_ids of all records that were created
1140
+ to prepare for rollback"""
1141
+ if records:
1142
+ table_name = Rollback._create_tables_for_rollback(
1143
+ context, step, RollbackType.INSERT
1144
+ )
1145
+ conn = context.session.connection()
1146
+ sql_bulk_insert_from_records(
1147
+ connection=conn,
1148
+ table=context.metadata.tables[table_name],
1149
+ columns=["Id"],
1150
+ record_iterable=records,
1151
+ )
1152
+
1153
+ @staticmethod
1154
+ def _perform_rollback(context, table: Table) -> None:
1155
+ """Perform rollback for insert operation"""
1156
+ sf_object = table.name.split(f"_{RollbackType.INSERT.value}")[0]
1157
+ records = context.session.query(table).all()
1158
+
1159
+ if records:
1160
+ context.logger.info(f"Deleting {sf_object} records")
1161
+ # Perform DELETE operation using get_dml_operation
1162
+ step = get_dml_operation(
1163
+ sobject=sf_object,
1164
+ operation=DataOperationType.DELETE,
1165
+ fields=["Id"],
1166
+ api_options={},
1167
+ context=context,
1168
+ api=Rollback._initialized_rollback_tables_api[table.name],
1169
+ volume=len(records),
1170
+ )
1171
+ step.start()
1172
+ step.load_records(records)
1173
+ step.end()
1174
+ context.logger.info("Done")
1175
+
1176
+
1177
+ class StepResultInfo(T.NamedTuple):
1178
+ """Represent a Step Result in a form easily convertible to JSON"""
1179
+
1180
+ sobject: str
1181
+ result: DataOperationJobResult
1182
+ record_type: str = None
1183
+
1184
+ def simplify(self):
1185
+ return {
1186
+ "sobject": self.sobject,
1187
+ "record_type": self.record_type,
1188
+ **self.result.simplify(),
1189
+ }
1190
+
1191
+
1192
+ class SetRecentlyViewedInfo(T.NamedTuple):
1193
+ """Did the set recently succeed or fail?"""
1194
+
1195
+ sobject: str
1196
+ error: T.Optional[Exception]