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.
- cumulusci/__about__.py +1 -0
- cumulusci/__init__.py +22 -0
- cumulusci/__main__.py +3 -0
- cumulusci/cli/__init__.py +0 -0
- cumulusci/cli/cci.py +244 -0
- cumulusci/cli/error.py +125 -0
- cumulusci/cli/flow.py +185 -0
- cumulusci/cli/logger.py +72 -0
- cumulusci/cli/org.py +692 -0
- cumulusci/cli/plan.py +181 -0
- cumulusci/cli/project.py +391 -0
- cumulusci/cli/robot.py +116 -0
- cumulusci/cli/runtime.py +190 -0
- cumulusci/cli/service.py +521 -0
- cumulusci/cli/task.py +295 -0
- cumulusci/cli/tests/__init__.py +0 -0
- cumulusci/cli/tests/test_cci.py +545 -0
- cumulusci/cli/tests/test_error.py +170 -0
- cumulusci/cli/tests/test_flow.py +276 -0
- cumulusci/cli/tests/test_logger.py +25 -0
- cumulusci/cli/tests/test_org.py +1438 -0
- cumulusci/cli/tests/test_plan.py +245 -0
- cumulusci/cli/tests/test_project.py +235 -0
- cumulusci/cli/tests/test_robot.py +177 -0
- cumulusci/cli/tests/test_runtime.py +197 -0
- cumulusci/cli/tests/test_service.py +853 -0
- cumulusci/cli/tests/test_task.py +266 -0
- cumulusci/cli/tests/test_ui.py +310 -0
- cumulusci/cli/tests/test_utils.py +122 -0
- cumulusci/cli/tests/utils.py +52 -0
- cumulusci/cli/ui.py +234 -0
- cumulusci/cli/utils.py +150 -0
- cumulusci/conftest.py +181 -0
- cumulusci/core/__init__.py +0 -0
- cumulusci/core/config/BaseConfig.py +5 -0
- cumulusci/core/config/BaseTaskFlowConfig.py +5 -0
- cumulusci/core/config/OrgConfig.py +5 -0
- cumulusci/core/config/ScratchOrgConfig.py +5 -0
- cumulusci/core/config/__init__.py +125 -0
- cumulusci/core/config/base_config.py +111 -0
- cumulusci/core/config/base_task_flow_config.py +82 -0
- cumulusci/core/config/marketing_cloud_service_config.py +83 -0
- cumulusci/core/config/oauth2_service_config.py +17 -0
- cumulusci/core/config/org_config.py +604 -0
- cumulusci/core/config/project_config.py +782 -0
- cumulusci/core/config/scratch_org_config.py +251 -0
- cumulusci/core/config/sfdx_org_config.py +220 -0
- cumulusci/core/config/tests/_test_config_backwards_compatibility.py +33 -0
- cumulusci/core/config/tests/test_config.py +1895 -0
- cumulusci/core/config/tests/test_config_expensive.py +839 -0
- cumulusci/core/config/tests/test_config_util.py +91 -0
- cumulusci/core/config/universal_config.py +88 -0
- cumulusci/core/config/util.py +18 -0
- cumulusci/core/datasets.py +303 -0
- cumulusci/core/debug.py +33 -0
- cumulusci/core/dependencies/__init__.py +55 -0
- cumulusci/core/dependencies/base.py +561 -0
- cumulusci/core/dependencies/dependencies.py +273 -0
- cumulusci/core/dependencies/github.py +177 -0
- cumulusci/core/dependencies/github_resolvers.py +244 -0
- cumulusci/core/dependencies/resolvers.py +580 -0
- cumulusci/core/dependencies/tests/__init__.py +0 -0
- cumulusci/core/dependencies/tests/conftest.py +385 -0
- cumulusci/core/dependencies/tests/test_dependencies.py +950 -0
- cumulusci/core/dependencies/tests/test_github.py +83 -0
- cumulusci/core/dependencies/tests/test_resolvers.py +1027 -0
- cumulusci/core/dependencies/utils.py +13 -0
- cumulusci/core/enums.py +11 -0
- cumulusci/core/exceptions.py +311 -0
- cumulusci/core/flowrunner.py +888 -0
- cumulusci/core/github.py +665 -0
- cumulusci/core/keychain/__init__.py +24 -0
- cumulusci/core/keychain/base_project_keychain.py +441 -0
- cumulusci/core/keychain/encrypted_file_project_keychain.py +945 -0
- cumulusci/core/keychain/environment_project_keychain.py +7 -0
- cumulusci/core/keychain/serialization.py +152 -0
- cumulusci/core/keychain/subprocess_keychain.py +24 -0
- cumulusci/core/keychain/tests/conftest.py +50 -0
- cumulusci/core/keychain/tests/test_base_project_keychain.py +299 -0
- cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +1228 -0
- cumulusci/core/metadeploy/__init__.py +0 -0
- cumulusci/core/metadeploy/api.py +88 -0
- cumulusci/core/metadeploy/plans.py +25 -0
- cumulusci/core/metadeploy/tests/test_api.py +276 -0
- cumulusci/core/runtime.py +115 -0
- cumulusci/core/sfdx.py +162 -0
- cumulusci/core/source/__init__.py +16 -0
- cumulusci/core/source/github.py +50 -0
- cumulusci/core/source/local_folder.py +35 -0
- cumulusci/core/source_transforms/__init__.py +0 -0
- cumulusci/core/source_transforms/tests/test_transforms.py +1091 -0
- cumulusci/core/source_transforms/transforms.py +532 -0
- cumulusci/core/tasks.py +404 -0
- cumulusci/core/template_utils.py +59 -0
- cumulusci/core/tests/__init__.py +0 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_e2e.yaml +215 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_extract_standard_objects.yaml +199 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_read_explicit_extract_declaration.yaml +3 -0
- cumulusci/core/tests/fake_remote_repo/cumulusci.yml +32 -0
- cumulusci/core/tests/fake_remote_repo/tasks/directory/example_2.py +6 -0
- cumulusci/core/tests/fake_remote_repo/tasks/example.py +43 -0
- cumulusci/core/tests/fake_remote_repo_2/cumulusci.yml +11 -0
- cumulusci/core/tests/fake_remote_repo_2/tasks/example_3.py +6 -0
- cumulusci/core/tests/test_datasets_e2e.py +386 -0
- cumulusci/core/tests/test_exceptions.py +11 -0
- cumulusci/core/tests/test_flowrunner.py +836 -0
- cumulusci/core/tests/test_github.py +942 -0
- cumulusci/core/tests/test_sfdx.py +138 -0
- cumulusci/core/tests/test_source.py +678 -0
- cumulusci/core/tests/test_tasks.py +262 -0
- cumulusci/core/tests/test_utils.py +141 -0
- cumulusci/core/tests/test_utils_merge_config.py +276 -0
- cumulusci/core/tests/test_versions.py +76 -0
- cumulusci/core/tests/untrusted_repo_child/cumulusci.yml +7 -0
- cumulusci/core/tests/untrusted_repo_child/tasks/untrusted_child.py +6 -0
- cumulusci/core/tests/untrusted_repo_parent/cumulusci.yml +26 -0
- cumulusci/core/tests/untrusted_repo_parent/tasks/untrusted_parent.py +6 -0
- cumulusci/core/tests/utils.py +116 -0
- cumulusci/core/tests/yaml/global.yaml +0 -0
- cumulusci/core/utils.py +402 -0
- cumulusci/core/versions.py +149 -0
- cumulusci/cumulusci.yml +1621 -0
- cumulusci/files/admin_profile.xml +20 -0
- cumulusci/files/delete_excludes.txt +424 -0
- cumulusci/files/templates/project/README.md +12 -0
- cumulusci/files/templates/project/cumulusci.yml +63 -0
- cumulusci/files/templates/project/dot-gitignore +60 -0
- cumulusci/files/templates/project/mapping.yml +45 -0
- cumulusci/files/templates/project/scratch_def.json +25 -0
- cumulusci/oauth/__init__.py +0 -0
- cumulusci/oauth/client.py +400 -0
- cumulusci/oauth/exceptions.py +9 -0
- cumulusci/oauth/salesforce.py +95 -0
- cumulusci/oauth/tests/__init__.py +0 -0
- cumulusci/oauth/tests/cassettes/test_get_device_code.yaml +22 -0
- cumulusci/oauth/tests/cassettes/test_get_device_oauth_token.yaml +74 -0
- cumulusci/oauth/tests/test_client.py +308 -0
- cumulusci/oauth/tests/test_salesforce.py +46 -0
- cumulusci/plugins/__init__.py +3 -0
- cumulusci/plugins/plugin_base.py +93 -0
- cumulusci/plugins/plugin_loader.py +59 -0
- cumulusci/robotframework/CumulusCI.py +340 -0
- cumulusci/robotframework/CumulusCI.robot +7 -0
- cumulusci/robotframework/Performance.py +165 -0
- cumulusci/robotframework/Salesforce.py +936 -0
- cumulusci/robotframework/Salesforce.robot +192 -0
- cumulusci/robotframework/SalesforceAPI.py +416 -0
- cumulusci/robotframework/SalesforcePlaywright.py +220 -0
- cumulusci/robotframework/SalesforcePlaywright.robot +40 -0
- cumulusci/robotframework/__init__.py +2 -0
- cumulusci/robotframework/base_library.py +39 -0
- cumulusci/robotframework/faker_mixin.py +89 -0
- cumulusci/robotframework/form_handlers.py +222 -0
- cumulusci/robotframework/javascript/cci_init.js +34 -0
- cumulusci/robotframework/javascript/cumulusci.js +4 -0
- cumulusci/robotframework/locator_manager.py +197 -0
- cumulusci/robotframework/locators_56.py +88 -0
- cumulusci/robotframework/locators_57.py +5 -0
- cumulusci/robotframework/pageobjects/BasePageObjects.py +433 -0
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +246 -0
- cumulusci/robotframework/pageobjects/PageObjectLibrary.py +45 -0
- cumulusci/robotframework/pageobjects/PageObjects.py +351 -0
- cumulusci/robotframework/pageobjects/__init__.py +12 -0
- cumulusci/robotframework/pageobjects/baseobjects.py +120 -0
- cumulusci/robotframework/perftests/short/collection_perf.robot +105 -0
- cumulusci/robotframework/tests/CustomObjectTestPage.py +10 -0
- cumulusci/robotframework/tests/FooTestPage.py +8 -0
- cumulusci/robotframework/tests/cumulusci/base.robot +40 -0
- cumulusci/robotframework/tests/cumulusci/bulkdata.robot +38 -0
- cumulusci/robotframework/tests/cumulusci/communities.robot +57 -0
- cumulusci/robotframework/tests/cumulusci/datagen.robot +84 -0
- cumulusci/robotframework/tests/salesforce/TestLibraryA.py +24 -0
- cumulusci/robotframework/tests/salesforce/TestLibraryB.py +20 -0
- cumulusci/robotframework/tests/salesforce/TestListener.py +93 -0
- cumulusci/robotframework/tests/salesforce/api.robot +178 -0
- cumulusci/robotframework/tests/salesforce/browsers.robot +143 -0
- cumulusci/robotframework/tests/salesforce/classic.robot +51 -0
- cumulusci/robotframework/tests/salesforce/create_contact.robot +59 -0
- cumulusci/robotframework/tests/salesforce/faker.robot +68 -0
- cumulusci/robotframework/tests/salesforce/forms.robot +172 -0
- cumulusci/robotframework/tests/salesforce/label_locator.robot +244 -0
- cumulusci/robotframework/tests/salesforce/labels.html +33 -0
- cumulusci/robotframework/tests/salesforce/locators.robot +149 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/base_pageobjects.robot +100 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/example_page_object.py +25 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/listing_page.robot +115 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/objectmanager.robot +74 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/pageobjects.robot +171 -0
- cumulusci/robotframework/tests/salesforce/performance.robot +109 -0
- cumulusci/robotframework/tests/salesforce/playwright/javascript_keywords.robot +33 -0
- cumulusci/robotframework/tests/salesforce/playwright/open_test_browser.robot +48 -0
- cumulusci/robotframework/tests/salesforce/playwright/playwright.robot +24 -0
- cumulusci/robotframework/tests/salesforce/playwright/ui.robot +32 -0
- cumulusci/robotframework/tests/salesforce/populate.robot +89 -0
- cumulusci/robotframework/tests/salesforce/test_testlistener.py +37 -0
- cumulusci/robotframework/tests/salesforce/ui.robot +361 -0
- cumulusci/robotframework/tests/test_cumulusci_library.py +304 -0
- cumulusci/robotframework/tests/test_locator_manager.py +158 -0
- cumulusci/robotframework/tests/test_pageobjects.py +291 -0
- cumulusci/robotframework/tests/test_performance.py +38 -0
- cumulusci/robotframework/tests/test_salesforce.py +79 -0
- cumulusci/robotframework/tests/test_salesforce_locators.py +73 -0
- cumulusci/robotframework/tests/test_template_util.py +53 -0
- cumulusci/robotframework/tests/test_utils.py +106 -0
- cumulusci/robotframework/utils.py +283 -0
- cumulusci/salesforce_api/__init__.py +0 -0
- cumulusci/salesforce_api/exceptions.py +23 -0
- cumulusci/salesforce_api/filterable_objects.py +96 -0
- cumulusci/salesforce_api/mc_soap_envelopes.py +89 -0
- cumulusci/salesforce_api/metadata.py +721 -0
- cumulusci/salesforce_api/org_schema.py +571 -0
- cumulusci/salesforce_api/org_schema_models.py +226 -0
- cumulusci/salesforce_api/package_install.py +265 -0
- cumulusci/salesforce_api/package_zip.py +301 -0
- cumulusci/salesforce_api/rest_deploy.py +148 -0
- cumulusci/salesforce_api/retrieve_profile_api.py +301 -0
- cumulusci/salesforce_api/soap_envelopes.py +177 -0
- cumulusci/salesforce_api/tests/__init__.py +0 -0
- cumulusci/salesforce_api/tests/metadata_test_strings.py +24 -0
- cumulusci/salesforce_api/tests/test_metadata.py +1015 -0
- cumulusci/salesforce_api/tests/test_package_install.py +219 -0
- cumulusci/salesforce_api/tests/test_package_zip.py +380 -0
- cumulusci/salesforce_api/tests/test_rest_deploy.py +264 -0
- cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +337 -0
- cumulusci/salesforce_api/tests/test_utils.py +124 -0
- cumulusci/salesforce_api/utils.py +51 -0
- cumulusci/schema/cumulusci.jsonschema.json +782 -0
- cumulusci/tasks/__init__.py +0 -0
- cumulusci/tasks/apex/__init__.py +0 -0
- cumulusci/tasks/apex/anon.py +157 -0
- cumulusci/tasks/apex/batch.py +180 -0
- cumulusci/tasks/apex/testrunner.py +835 -0
- cumulusci/tasks/apex/tests/cassettes/ManualEditTestApexIntegrationTests.test_run_tests__integration_test.yaml +703 -0
- cumulusci/tasks/apex/tests/test_apex_tasks.py +1558 -0
- cumulusci/tasks/base_source_control_task.py +17 -0
- cumulusci/tasks/bulkdata/__init__.py +15 -0
- cumulusci/tasks/bulkdata/base_generate_data_task.py +96 -0
- cumulusci/tasks/bulkdata/dates.py +97 -0
- cumulusci/tasks/bulkdata/delete.py +156 -0
- cumulusci/tasks/bulkdata/extract.py +441 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/calculate_dependencies.py +117 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +123 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/hardcoded_default_declarations.py +49 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +283 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +142 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py +575 -0
- cumulusci/tasks/bulkdata/factory_utils.py +134 -0
- cumulusci/tasks/bulkdata/generate.py +4 -0
- cumulusci/tasks/bulkdata/generate_and_load_data.py +232 -0
- cumulusci/tasks/bulkdata/generate_and_load_data_from_yaml.py +19 -0
- cumulusci/tasks/bulkdata/generate_from_yaml.py +183 -0
- cumulusci/tasks/bulkdata/generate_mapping.py +434 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py +169 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/extract_mapping_file_generator.py +45 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/generate_mapping_from_declarations.py +121 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/load_mapping_file_generator.py +127 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_generator_post_processes.py +53 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_transforms.py +139 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py +135 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_load_mapping_from_declarations.py +330 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_generator_post_processes.py +60 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_transforms.py +188 -0
- cumulusci/tasks/bulkdata/load.py +1196 -0
- cumulusci/tasks/bulkdata/mapping_parser.py +811 -0
- cumulusci/tasks/bulkdata/query_transformers.py +264 -0
- cumulusci/tasks/bulkdata/select_utils.py +792 -0
- cumulusci/tasks/bulkdata/snowfakery.py +753 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/queue_manager.py +478 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_run_until.py +141 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_working_directory.py +53 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/subtask_configurator.py +64 -0
- cumulusci/tasks/bulkdata/step.py +1242 -0
- cumulusci/tasks/bulkdata/tests/__init__.py +0 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_random_strategy.yaml +147 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_annoy_strategy.yaml +123 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy.yaml +313 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy_bulk.yaml +550 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_strategy.yaml +175 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_standard_strategy.yaml +147 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__multiple_needed.yaml +69 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__none_needed.yaml +22 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__one_needed.yaml +24 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_snowfakery_query_salesforce.yaml +25 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpdatesIntegrationTests.test_updates_task.yaml +80 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_simple_upsert__rest.yaml +270 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert__rest.yaml +267 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field__rest.yaml +369 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field_rest__duplicate_error.yaml +204 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_fields__bulk.yaml +675 -0
- cumulusci/tasks/bulkdata/tests/dummy_data_factory.py +36 -0
- cumulusci/tasks/bulkdata/tests/integration_test_utils.py +49 -0
- cumulusci/tasks/bulkdata/tests/mapping-oid.yml +87 -0
- cumulusci/tasks/bulkdata/tests/mapping_after.yml +38 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly.yml +34 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly_incomplete.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly_wrong.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_strategy.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_number.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_strategy.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__non_float.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_missing_priority_fields.yml +22 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_no_priority_fields.yml +18 -0
- cumulusci/tasks/bulkdata/tests/mapping_simple.yml +27 -0
- cumulusci/tasks/bulkdata/tests/mapping_v1.yml +28 -0
- cumulusci/tasks/bulkdata/tests/mapping_v2.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_v3.yml +32 -0
- cumulusci/tasks/bulkdata/tests/mapping_vanilla_sf.yml +69 -0
- cumulusci/tasks/bulkdata/tests/mock_data_factory_without_mapping.py +12 -0
- cumulusci/tasks/bulkdata/tests/person_accounts.yml +23 -0
- cumulusci/tasks/bulkdata/tests/person_accounts_minimal.yml +15 -0
- cumulusci/tasks/bulkdata/tests/recordtypes.yml +8 -0
- cumulusci/tasks/bulkdata/tests/recordtypes_2.yml +6 -0
- cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.yml +8 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/child/child2.yml +3 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/child.yml +4 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/gen_npsp_standard_objects.recipe.yml +89 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/include_parent.yml +3 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/npsp_standard_objects_macros.yml +34 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/options.recipe.yml +6 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml +16 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/sf_standard_object_macros.yml +83 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.load.yml +2 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.recipe.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_2.load.yml +5 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.load.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.recipe.yml +12 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels_2.load.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/unique_values.recipe.yml +4 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert.recipe.yml +23 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert_2.recipe.yml +29 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert_before.yml +10 -0
- cumulusci/tasks/bulkdata/tests/test_base_generate_data_tasks.py +61 -0
- cumulusci/tasks/bulkdata/tests/test_dates.py +99 -0
- cumulusci/tasks/bulkdata/tests/test_delete.py +404 -0
- cumulusci/tasks/bulkdata/tests/test_extract.py +1311 -0
- cumulusci/tasks/bulkdata/tests/test_factory_utils.py +55 -0
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +252 -0
- cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py +343 -0
- cumulusci/tasks/bulkdata/tests/test_generatemapping.py +1039 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +3175 -0
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +1658 -0
- cumulusci/tasks/bulkdata/tests/test_query_db__joins_self_lookups.yml +12 -0
- cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups.yml +26 -0
- cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups_select.yml +48 -0
- cumulusci/tasks/bulkdata/tests/test_select.py +171 -0
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +1057 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +1153 -0
- cumulusci/tasks/bulkdata/tests/test_step.py +3957 -0
- cumulusci/tasks/bulkdata/tests/test_updates.py +513 -0
- cumulusci/tasks/bulkdata/tests/test_upsert.py +1015 -0
- cumulusci/tasks/bulkdata/tests/test_utils.py +158 -0
- cumulusci/tasks/bulkdata/tests/testdata.db +0 -0
- cumulusci/tasks/bulkdata/tests/update_describe.py +50 -0
- cumulusci/tasks/bulkdata/tests/update_person_accounts.yml +23 -0
- cumulusci/tasks/bulkdata/tests/utils.py +114 -0
- cumulusci/tasks/bulkdata/update_data.py +260 -0
- cumulusci/tasks/bulkdata/upsert_utils.py +130 -0
- cumulusci/tasks/bulkdata/utils.py +249 -0
- cumulusci/tasks/command.py +178 -0
- cumulusci/tasks/connectedapp.py +186 -0
- cumulusci/tasks/create_package_version.py +778 -0
- cumulusci/tasks/datadictionary.py +745 -0
- cumulusci/tasks/dx_convert_from.py +26 -0
- cumulusci/tasks/github/__init__.py +17 -0
- cumulusci/tasks/github/base.py +16 -0
- cumulusci/tasks/github/commit_status.py +13 -0
- cumulusci/tasks/github/merge.py +11 -0
- cumulusci/tasks/github/publish.py +11 -0
- cumulusci/tasks/github/pull_request.py +11 -0
- cumulusci/tasks/github/release.py +11 -0
- cumulusci/tasks/github/release_report.py +11 -0
- cumulusci/tasks/github/tag.py +11 -0
- cumulusci/tasks/github/tests/__init__.py +0 -0
- cumulusci/tasks/github/tests/test_util.py +202 -0
- cumulusci/tasks/github/tests/test_vcs_migration.py +44 -0
- cumulusci/tasks/github/tests/util_github_api.py +666 -0
- cumulusci/tasks/github/util.py +252 -0
- cumulusci/tasks/marketing_cloud/__init__.py +0 -0
- cumulusci/tasks/marketing_cloud/api.py +188 -0
- cumulusci/tasks/marketing_cloud/base.py +38 -0
- cumulusci/tasks/marketing_cloud/deploy.py +345 -0
- cumulusci/tasks/marketing_cloud/get_user_info.py +40 -0
- cumulusci/tasks/marketing_cloud/mc_constants.py +1 -0
- cumulusci/tasks/marketing_cloud/tests/__init__.py +0 -0
- cumulusci/tasks/marketing_cloud/tests/conftest.py +46 -0
- cumulusci/tasks/marketing_cloud/tests/expected-payload.json +110 -0
- cumulusci/tasks/marketing_cloud/tests/test_api.py +97 -0
- cumulusci/tasks/marketing_cloud/tests/test_api_soap_envelopes.py +145 -0
- cumulusci/tasks/marketing_cloud/tests/test_base.py +14 -0
- cumulusci/tasks/marketing_cloud/tests/test_deploy.py +400 -0
- cumulusci/tasks/marketing_cloud/tests/test_get_user_info.py +141 -0
- cumulusci/tasks/marketing_cloud/tests/validation-response.json +39 -0
- cumulusci/tasks/metadata/__init__.py +0 -0
- cumulusci/tasks/metadata/ee_src.py +94 -0
- cumulusci/tasks/metadata/managed_src.py +100 -0
- cumulusci/tasks/metadata/metadata_map.yml +868 -0
- cumulusci/tasks/metadata/modify.py +99 -0
- cumulusci/tasks/metadata/package.py +684 -0
- cumulusci/tasks/metadata/tests/__init__.py +0 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/.hidden/.keep +0 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/destructiveChanges.xml +9 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package.xml +9 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package_install_uninstall.xml +11 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/reports/namespace__TestFolder/TestReport.report +3 -0
- cumulusci/tasks/metadata/tests/sample_package.xml +9 -0
- cumulusci/tasks/metadata/tests/test_ee_src.py +112 -0
- cumulusci/tasks/metadata/tests/test_managed_src.py +111 -0
- cumulusci/tasks/metadata/tests/test_modify.py +123 -0
- cumulusci/tasks/metadata/tests/test_package.py +476 -0
- cumulusci/tasks/metadata_etl/__init__.py +29 -0
- cumulusci/tasks/metadata_etl/base.py +436 -0
- cumulusci/tasks/metadata_etl/duplicate_rules.py +24 -0
- cumulusci/tasks/metadata_etl/field_sets.py +70 -0
- cumulusci/tasks/metadata_etl/help_text.py +92 -0
- cumulusci/tasks/metadata_etl/layouts.py +550 -0
- cumulusci/tasks/metadata_etl/objects.py +68 -0
- cumulusci/tasks/metadata_etl/permissions.py +167 -0
- cumulusci/tasks/metadata_etl/picklists.py +221 -0
- cumulusci/tasks/metadata_etl/remote_site_settings.py +99 -0
- cumulusci/tasks/metadata_etl/sharing.py +138 -0
- cumulusci/tasks/metadata_etl/tests/test_base.py +512 -0
- cumulusci/tasks/metadata_etl/tests/test_duplicate_rules.py +22 -0
- cumulusci/tasks/metadata_etl/tests/test_field_sets.py +156 -0
- cumulusci/tasks/metadata_etl/tests/test_help_text.py +387 -0
- cumulusci/tasks/metadata_etl/tests/test_ip_ranges.py +85 -0
- cumulusci/tasks/metadata_etl/tests/test_layouts.py +858 -0
- cumulusci/tasks/metadata_etl/tests/test_objects.py +236 -0
- cumulusci/tasks/metadata_etl/tests/test_permissions.py +223 -0
- cumulusci/tasks/metadata_etl/tests/test_picklists.py +547 -0
- cumulusci/tasks/metadata_etl/tests/test_remote_site_settings.py +46 -0
- cumulusci/tasks/metadata_etl/tests/test_sharing.py +333 -0
- cumulusci/tasks/metadata_etl/tests/test_value_sets.py +298 -0
- cumulusci/tasks/metadata_etl/value_sets.py +106 -0
- cumulusci/tasks/metadeploy.py +393 -0
- cumulusci/tasks/metaxml.py +88 -0
- cumulusci/tasks/preflight/__init__.py +0 -0
- cumulusci/tasks/preflight/dataset_load.py +49 -0
- cumulusci/tasks/preflight/licenses.py +86 -0
- cumulusci/tasks/preflight/packages.py +14 -0
- cumulusci/tasks/preflight/permsets.py +23 -0
- cumulusci/tasks/preflight/recordtypes.py +16 -0
- cumulusci/tasks/preflight/retrieve_tasks.py +30 -0
- cumulusci/tasks/preflight/settings.py +77 -0
- cumulusci/tasks/preflight/sobjects.py +202 -0
- cumulusci/tasks/preflight/tests/test_dataset_load.py +85 -0
- cumulusci/tasks/preflight/tests/test_licenses.py +174 -0
- cumulusci/tasks/preflight/tests/test_packages.py +14 -0
- cumulusci/tasks/preflight/tests/test_permset_preflights.py +51 -0
- cumulusci/tasks/preflight/tests/test_recordtypes.py +30 -0
- cumulusci/tasks/preflight/tests/test_retrieve_tasks.py +62 -0
- cumulusci/tasks/preflight/tests/test_settings.py +130 -0
- cumulusci/tasks/preflight/tests/test_sobjects.py +231 -0
- cumulusci/tasks/push/README.md +59 -0
- cumulusci/tasks/push/__init__.py +0 -0
- cumulusci/tasks/push/push_api.py +659 -0
- cumulusci/tasks/push/pushfails.py +136 -0
- cumulusci/tasks/push/tasks.py +476 -0
- cumulusci/tasks/push/tests/conftest.py +263 -0
- cumulusci/tasks/push/tests/test_push_api.py +951 -0
- cumulusci/tasks/push/tests/test_push_tasks.py +659 -0
- cumulusci/tasks/release_notes/README.md +63 -0
- cumulusci/tasks/release_notes/__init__.py +0 -0
- cumulusci/tasks/release_notes/exceptions.py +5 -0
- cumulusci/tasks/release_notes/generator.py +137 -0
- cumulusci/tasks/release_notes/parser.py +232 -0
- cumulusci/tasks/release_notes/provider.py +44 -0
- cumulusci/tasks/release_notes/task.py +300 -0
- cumulusci/tasks/release_notes/tests/__init__.py +0 -0
- cumulusci/tasks/release_notes/tests/change_notes/full/example1.md +17 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/1.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/2.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/3.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/single/1.txt +1 -0
- cumulusci/tasks/release_notes/tests/test_generator.py +582 -0
- cumulusci/tasks/release_notes/tests/test_parser.py +867 -0
- cumulusci/tasks/release_notes/tests/test_provider.py +512 -0
- cumulusci/tasks/release_notes/tests/test_task.py +461 -0
- cumulusci/tasks/release_notes/tests/utils.py +153 -0
- cumulusci/tasks/robotframework/__init__.py +3 -0
- cumulusci/tasks/robotframework/debugger/DebugListener.py +100 -0
- cumulusci/tasks/robotframework/debugger/__init__.py +10 -0
- cumulusci/tasks/robotframework/debugger/model.py +87 -0
- cumulusci/tasks/robotframework/debugger/ui.py +259 -0
- cumulusci/tasks/robotframework/libdoc.py +269 -0
- cumulusci/tasks/robotframework/robotframework.py +392 -0
- cumulusci/tasks/robotframework/stylesheet.css +130 -0
- cumulusci/tasks/robotframework/template.html +109 -0
- cumulusci/tasks/robotframework/tests/TestLibrary.py +18 -0
- cumulusci/tasks/robotframework/tests/TestPageObjects.py +31 -0
- cumulusci/tasks/robotframework/tests/TestResource.robot +8 -0
- cumulusci/tasks/robotframework/tests/failing_tests.robot +16 -0
- cumulusci/tasks/robotframework/tests/performance.robot +23 -0
- cumulusci/tasks/robotframework/tests/test_browser_proxies.py +137 -0
- cumulusci/tasks/robotframework/tests/test_debugger.py +360 -0
- cumulusci/tasks/robotframework/tests/test_robot_parallel.py +141 -0
- cumulusci/tasks/robotframework/tests/test_robotframework.py +860 -0
- cumulusci/tasks/salesforce/BaseRetrieveMetadata.py +58 -0
- cumulusci/tasks/salesforce/BaseSalesforceApiTask.py +45 -0
- cumulusci/tasks/salesforce/BaseSalesforceMetadataApiTask.py +18 -0
- cumulusci/tasks/salesforce/BaseSalesforceTask.py +4 -0
- cumulusci/tasks/salesforce/BaseUninstallMetadata.py +41 -0
- cumulusci/tasks/salesforce/CreateCommunity.py +124 -0
- cumulusci/tasks/salesforce/CreatePackage.py +29 -0
- cumulusci/tasks/salesforce/Deploy.py +240 -0
- cumulusci/tasks/salesforce/DeployBundles.py +88 -0
- cumulusci/tasks/salesforce/DescribeMetadataTypes.py +26 -0
- cumulusci/tasks/salesforce/EnsureRecordTypes.py +202 -0
- cumulusci/tasks/salesforce/GetInstalledPackages.py +8 -0
- cumulusci/tasks/salesforce/ListCommunities.py +40 -0
- cumulusci/tasks/salesforce/ListCommunityTemplates.py +19 -0
- cumulusci/tasks/salesforce/PublishCommunity.py +62 -0
- cumulusci/tasks/salesforce/RetrievePackaged.py +41 -0
- cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py +82 -0
- cumulusci/tasks/salesforce/RetrieveUnpackaged.py +36 -0
- cumulusci/tasks/salesforce/SOQLQuery.py +39 -0
- cumulusci/tasks/salesforce/UninstallLocal.py +15 -0
- cumulusci/tasks/salesforce/UninstallLocalBundles.py +28 -0
- cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py +58 -0
- cumulusci/tasks/salesforce/UninstallPackage.py +32 -0
- cumulusci/tasks/salesforce/UninstallPackaged.py +56 -0
- cumulusci/tasks/salesforce/UpdateAdminProfile.py +8 -0
- cumulusci/tasks/salesforce/__init__.py +79 -0
- cumulusci/tasks/salesforce/activate_flow.py +74 -0
- cumulusci/tasks/salesforce/check_components.py +324 -0
- cumulusci/tasks/salesforce/composite.py +142 -0
- cumulusci/tasks/salesforce/create_permission_sets.py +35 -0
- cumulusci/tasks/salesforce/custom_settings.py +134 -0
- cumulusci/tasks/salesforce/custom_settings_wait.py +132 -0
- cumulusci/tasks/salesforce/enable_prediction.py +107 -0
- cumulusci/tasks/salesforce/insert_record.py +40 -0
- cumulusci/tasks/salesforce/install_package_version.py +242 -0
- cumulusci/tasks/salesforce/license_preflights.py +8 -0
- cumulusci/tasks/salesforce/network_member_group.py +178 -0
- cumulusci/tasks/salesforce/nonsourcetracking.py +228 -0
- cumulusci/tasks/salesforce/org_settings.py +193 -0
- cumulusci/tasks/salesforce/package_upload.py +328 -0
- cumulusci/tasks/salesforce/profiles.py +74 -0
- cumulusci/tasks/salesforce/promote_package_version.py +376 -0
- cumulusci/tasks/salesforce/retrieve_profile.py +195 -0
- cumulusci/tasks/salesforce/salesforce_files.py +244 -0
- cumulusci/tasks/salesforce/sourcetracking.py +507 -0
- cumulusci/tasks/salesforce/tests/__init__.py +3 -0
- cumulusci/tasks/salesforce/tests/test_CreateCommunity.py +278 -0
- cumulusci/tasks/salesforce/tests/test_CreatePackage.py +22 -0
- cumulusci/tasks/salesforce/tests/test_Deploy.py +470 -0
- cumulusci/tasks/salesforce/tests/test_DeployBundles.py +76 -0
- cumulusci/tasks/salesforce/tests/test_EnsureRecordTypes.py +345 -0
- cumulusci/tasks/salesforce/tests/test_ListCommunities.py +84 -0
- cumulusci/tasks/salesforce/tests/test_ListCommunityTemplates.py +49 -0
- cumulusci/tasks/salesforce/tests/test_PackageUpload.py +547 -0
- cumulusci/tasks/salesforce/tests/test_ProfileGrantAllAccess.py +699 -0
- cumulusci/tasks/salesforce/tests/test_PublishCommunity.py +181 -0
- cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py +24 -0
- cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py +56 -0
- cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py +21 -0
- cumulusci/tasks/salesforce/tests/test_SOQLQuery.py +30 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocal.py +15 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py +19 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py +22 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackage.py +19 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackaged.py +66 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackagedIncremental.py +127 -0
- cumulusci/tasks/salesforce/tests/test_activate_flow.py +132 -0
- cumulusci/tasks/salesforce/tests/test_base_tasks.py +110 -0
- cumulusci/tasks/salesforce/tests/test_check_components.py +445 -0
- cumulusci/tasks/salesforce/tests/test_composite.py +250 -0
- cumulusci/tasks/salesforce/tests/test_create_permission_sets.py +41 -0
- cumulusci/tasks/salesforce/tests/test_custom_settings.py +227 -0
- cumulusci/tasks/salesforce/tests/test_custom_settings_wait.py +174 -0
- cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py +18 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +240 -0
- cumulusci/tasks/salesforce/tests/test_insert_record.py +110 -0
- cumulusci/tasks/salesforce/tests/test_install_package_version.py +464 -0
- cumulusci/tasks/salesforce/tests/test_network_member_group.py +444 -0
- cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py +235 -0
- cumulusci/tasks/salesforce/tests/test_org_settings.py +407 -0
- cumulusci/tasks/salesforce/tests/test_profiles.py +202 -0
- cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +287 -0
- cumulusci/tasks/salesforce/tests/test_salesforce_files.py +228 -0
- cumulusci/tasks/salesforce/tests/test_sourcetracking.py +350 -0
- cumulusci/tasks/salesforce/tests/test_trigger_handlers.py +300 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +509 -0
- cumulusci/tasks/salesforce/tests/util.py +79 -0
- cumulusci/tasks/salesforce/trigger_handlers.py +119 -0
- cumulusci/tasks/salesforce/uninstall_packaged_incremental.py +136 -0
- cumulusci/tasks/salesforce/update_dependencies.py +290 -0
- cumulusci/tasks/salesforce/update_profile.py +339 -0
- cumulusci/tasks/salesforce/users/permsets.py +227 -0
- cumulusci/tasks/salesforce/users/photos.py +162 -0
- cumulusci/tasks/salesforce/users/tests/photo.mock.txt +1 -0
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +950 -0
- cumulusci/tasks/salesforce/users/tests/test_photos.py +373 -0
- cumulusci/tasks/sample_data/capture_sample_data.py +77 -0
- cumulusci/tasks/sample_data/load_sample_data.py +85 -0
- cumulusci/tasks/sample_data/test_capture_sample_data.py +117 -0
- cumulusci/tasks/sample_data/test_load_sample_data.py +121 -0
- cumulusci/tasks/sfdx.py +83 -0
- cumulusci/tasks/tests/__init__.py +1 -0
- cumulusci/tasks/tests/conftest.py +30 -0
- cumulusci/tasks/tests/test_command.py +129 -0
- cumulusci/tasks/tests/test_connectedapp.py +236 -0
- cumulusci/tasks/tests/test_create_package_version.py +847 -0
- cumulusci/tasks/tests/test_datadictionary.py +1575 -0
- cumulusci/tasks/tests/test_dx_convert_from.py +60 -0
- cumulusci/tasks/tests/test_metadeploy.py +624 -0
- cumulusci/tasks/tests/test_metaxml.py +99 -0
- cumulusci/tasks/tests/test_promote_package_version.py +488 -0
- cumulusci/tasks/tests/test_pushfails.py +96 -0
- cumulusci/tasks/tests/test_salesforce.py +72 -0
- cumulusci/tasks/tests/test_sfdx.py +105 -0
- cumulusci/tasks/tests/test_util.py +207 -0
- cumulusci/tasks/util.py +261 -0
- cumulusci/tasks/vcs/__init__.py +19 -0
- cumulusci/tasks/vcs/commit_status.py +58 -0
- cumulusci/tasks/vcs/create_commit_status.py +37 -0
- cumulusci/tasks/vcs/download_extract.py +199 -0
- cumulusci/tasks/vcs/merge.py +298 -0
- cumulusci/tasks/vcs/publish.py +207 -0
- cumulusci/tasks/vcs/pull_request.py +9 -0
- cumulusci/tasks/vcs/release.py +134 -0
- cumulusci/tasks/vcs/release_report.py +105 -0
- cumulusci/tasks/vcs/tag.py +31 -0
- cumulusci/tasks/vcs/tests/github/test_commit_status.py +196 -0
- cumulusci/tasks/vcs/tests/github/test_download_extract.py +896 -0
- cumulusci/tasks/vcs/tests/github/test_merge.py +1118 -0
- cumulusci/tasks/vcs/tests/github/test_publish.py +823 -0
- cumulusci/tasks/vcs/tests/github/test_pull_request.py +29 -0
- cumulusci/tasks/vcs/tests/github/test_release.py +390 -0
- cumulusci/tasks/vcs/tests/github/test_release_report.py +109 -0
- cumulusci/tasks/vcs/tests/github/test_tag.py +90 -0
- cumulusci/tasks/vlocity/exceptions.py +2 -0
- cumulusci/tasks/vlocity/tests/test_vlocity.py +283 -0
- cumulusci/tasks/vlocity/vlocity.py +342 -0
- cumulusci/tests/__init__.py +1 -0
- cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml +18 -0
- cumulusci/tests/cassettes/TestIntegrationInfrastructure.test_integration_tests.yaml +19 -0
- cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py +307 -0
- cumulusci/tests/pytest_plugins/pytest_sf_vcr.py +275 -0
- cumulusci/tests/pytest_plugins/pytest_sf_vcr_serializer.py +160 -0
- cumulusci/tests/pytest_plugins/pytest_typeguard.py +5 -0
- cumulusci/tests/pytest_plugins/test_vcr_string_compressor.py +49 -0
- cumulusci/tests/pytest_plugins/vcr_string_compressor.py +97 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Account_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Case_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Contact_describe.yaml +4838 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Custom__c_describe.yaml +242 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Event_describe.yaml +19 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Global_describe.yaml +1338 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Lead_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_OpportunityContactRole_describe.yaml +34 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Opportunity_describe.yaml +1261 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Organization.yaml +49 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfoList_xml.tpl +15 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfo_xml.tpl +13 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_insert_xml.tpl +24 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_upsert_xml.tpl +25 -0
- cumulusci/tests/test_entry_points.py +20 -0
- cumulusci/tests/test_integration_infrastructure.py +131 -0
- cumulusci/tests/test_main.py +9 -0
- cumulusci/tests/test_schema.py +32 -0
- cumulusci/tests/test_utils.py +657 -0
- cumulusci/tests/test_vcr_serializer.py +134 -0
- cumulusci/tests/uncompressed_cassette.yaml +83 -0
- cumulusci/tests/util.py +344 -0
- cumulusci/utils/__init__.py +731 -0
- cumulusci/utils/classutils.py +9 -0
- cumulusci/utils/collections.py +32 -0
- cumulusci/utils/deprecation.py +11 -0
- cumulusci/utils/encryption.py +31 -0
- cumulusci/utils/fileutils.py +295 -0
- cumulusci/utils/git.py +142 -0
- cumulusci/utils/http/multi_request.py +214 -0
- cumulusci/utils/http/requests_utils.py +103 -0
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +32 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_composite_parallel_salesforce.yaml +65 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_errors.yaml +24 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_reference_ids.yaml +49 -0
- cumulusci/utils/http/tests/test_multi_request.py +255 -0
- cumulusci/utils/iterators.py +21 -0
- cumulusci/utils/logging.py +128 -0
- cumulusci/utils/metaprogramming.py +10 -0
- cumulusci/utils/options.py +138 -0
- cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py +29 -0
- cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py +50 -0
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +238 -0
- cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py +243 -0
- cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py +353 -0
- cumulusci/utils/salesforce/count_sobjects.py +46 -0
- cumulusci/utils/salesforce/soql.py +17 -0
- cumulusci/utils/salesforce/tests/cassettes/ManualEdit_TestCountSObjects.test_count_sobjects__network_errors.yaml +23 -0
- cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects__errors.yaml +33 -0
- cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects_simple.yaml +29 -0
- cumulusci/utils/salesforce/tests/test_count_sobjects.py +29 -0
- cumulusci/utils/salesforce/tests/test_soql.py +30 -0
- cumulusci/utils/tests/cassettes/ManualEditTestDescribeOrg.test_minimal_schema.yaml +36 -0
- cumulusci/utils/tests/cassettes/ManualEdit_test_describe_to_sql.yaml +191 -0
- cumulusci/utils/tests/test_fileutils.py +284 -0
- cumulusci/utils/tests/test_git.py +85 -0
- cumulusci/utils/tests/test_logging.py +70 -0
- cumulusci/utils/tests/test_option_parsing.py +188 -0
- cumulusci/utils/tests/test_org_schema.py +691 -0
- cumulusci/utils/tests/test_org_schema_models.py +79 -0
- cumulusci/utils/tests/test_waiting.py +25 -0
- cumulusci/utils/version_strings.py +391 -0
- cumulusci/utils/waiting.py +42 -0
- cumulusci/utils/xml/__init__.py +91 -0
- cumulusci/utils/xml/metadata_tree.py +299 -0
- cumulusci/utils/xml/robot_xml.py +114 -0
- cumulusci/utils/xml/salesforce_encoding.py +100 -0
- cumulusci/utils/xml/test/test_metadata_tree.py +251 -0
- cumulusci/utils/xml/test/test_salesforce_encoding.py +173 -0
- cumulusci/utils/yaml/cumulusci_yml.py +401 -0
- cumulusci/utils/yaml/model_parser.py +156 -0
- cumulusci/utils/yaml/safer_loader.py +74 -0
- cumulusci/utils/yaml/tests/bad_cci.yml +5 -0
- cumulusci/utils/yaml/tests/cassettes/TestCumulusciYml.test_validate_url__with_errors.yaml +20 -0
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +286 -0
- cumulusci/utils/yaml/tests/test_model_parser.py +175 -0
- cumulusci/utils/yaml/tests/test_safer_loader.py +88 -0
- cumulusci/utils/ziputils.py +61 -0
- cumulusci/vcs/base.py +143 -0
- cumulusci/vcs/bootstrap.py +272 -0
- cumulusci/vcs/github/__init__.py +24 -0
- cumulusci/vcs/github/adapter.py +689 -0
- cumulusci/vcs/github/release_notes/generator.py +219 -0
- cumulusci/vcs/github/release_notes/parser.py +151 -0
- cumulusci/vcs/github/release_notes/provider.py +143 -0
- cumulusci/vcs/github/service.py +569 -0
- cumulusci/vcs/github/tests/test_adapter.py +138 -0
- cumulusci/vcs/github/tests/test_service.py +408 -0
- cumulusci/vcs/models.py +586 -0
- cumulusci/vcs/tests/conftest.py +41 -0
- cumulusci/vcs/tests/dummy_service.py +241 -0
- cumulusci/vcs/tests/test_vcs_base.py +687 -0
- cumulusci/vcs/tests/test_vcs_bootstrap.py +727 -0
- cumulusci/vcs/utils/__init__.py +31 -0
- cumulusci/vcs/vcs_source.py +287 -0
- cumulusci_plus-5.0.0.dist-info/METADATA +145 -0
- cumulusci_plus-5.0.0.dist-info/RECORD +744 -0
- cumulusci_plus-5.0.0.dist-info/WHEEL +4 -0
- cumulusci_plus-5.0.0.dist-info/entry_points.txt +3 -0
- cumulusci_plus-5.0.0.dist-info/licenses/AUTHORS.rst +41 -0
- 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]
|