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,753 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import time
|
|
3
|
+
import typing as T
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from math import ceil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from queue import Empty
|
|
10
|
+
from tempfile import TemporaryDirectory, mkdtemp
|
|
11
|
+
|
|
12
|
+
import psutil
|
|
13
|
+
from snowfakery.api import COUNT_REPS, infer_load_file_path
|
|
14
|
+
from snowfakery.cci_mapping_files.declaration_parser import (
|
|
15
|
+
ChannelDeclaration,
|
|
16
|
+
SObjectRuleDeclarationFile,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
import cumulusci.core.exceptions as exc
|
|
20
|
+
from cumulusci.core.config import OrgConfig, TaskConfig
|
|
21
|
+
from cumulusci.core.debug import get_debug_mode
|
|
22
|
+
from cumulusci.core.exceptions import TaskOptionsError
|
|
23
|
+
from cumulusci.core.keychain import BaseProjectKeychain
|
|
24
|
+
from cumulusci.core.utils import (
|
|
25
|
+
format_duration,
|
|
26
|
+
process_bool_arg,
|
|
27
|
+
process_list_arg,
|
|
28
|
+
process_list_of_pairs_dict_arg,
|
|
29
|
+
)
|
|
30
|
+
from cumulusci.tasks.bulkdata.generate_and_load_data_from_yaml import (
|
|
31
|
+
GenerateAndLoadDataFromYaml,
|
|
32
|
+
)
|
|
33
|
+
from cumulusci.tasks.salesforce import BaseSalesforceApiTask
|
|
34
|
+
|
|
35
|
+
from .snowfakery_utils.queue_manager import (
|
|
36
|
+
SnowfakeryChannelManager,
|
|
37
|
+
data_loader_new_directory_name,
|
|
38
|
+
)
|
|
39
|
+
from .snowfakery_utils.snowfakery_run_until import PortionGenerator, determine_run_until
|
|
40
|
+
from .snowfakery_utils.snowfakery_working_directory import SnowfakeryWorkingDirectory
|
|
41
|
+
from .snowfakery_utils.subtask_configurator import SubtaskConfigurator
|
|
42
|
+
|
|
43
|
+
# A portion serves the same process in this system as a "batch" in
|
|
44
|
+
# other systems. The term "batch" is not used to avoid confusion with
|
|
45
|
+
# Salesforce Bulk API 1.0 batches. For example, a portion of 250_000
|
|
46
|
+
# Account records would be broken into roughly 25 Salesforce upload
|
|
47
|
+
# batches.
|
|
48
|
+
|
|
49
|
+
# The system starts at the MIN_PORTION_SIZE and grows towards the
|
|
50
|
+
# MAX_PORTION_SIZE. This is to prevent the org wasting time waiting
|
|
51
|
+
# for the first portions.
|
|
52
|
+
MIN_PORTION_SIZE = 2_000
|
|
53
|
+
MAX_PORTION_SIZE = 250_000
|
|
54
|
+
ERROR_THRESHOLD = (
|
|
55
|
+
0 # TODO v2.1: Allow this to be a percentage of recent records instead
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# time between "ticks" where the task re-evaluates its progress
|
|
59
|
+
# relatively arbitrary trade-off between busy-waiting and adding latency.
|
|
60
|
+
WAIT_TIME = 3
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Snowfakery(BaseSalesforceApiTask):
|
|
64
|
+
|
|
65
|
+
task_docs = """
|
|
66
|
+
Do a data load with Snowfakery.
|
|
67
|
+
|
|
68
|
+
All options are optional.
|
|
69
|
+
|
|
70
|
+
The most commonly supplied options are `recipe` and one of the three
|
|
71
|
+
`run_until_...` options.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
task_options = {
|
|
75
|
+
"recipe": {
|
|
76
|
+
"required": True,
|
|
77
|
+
"description": "Path to a Snowfakery recipe file determining what data to generate and load.",
|
|
78
|
+
},
|
|
79
|
+
"run_until_records_in_org": {
|
|
80
|
+
"description": """<sobject>:<count>
|
|
81
|
+
|
|
82
|
+
Run the recipe repeatedly until the count of <sobject>
|
|
83
|
+
in the org matches the given <count>.
|
|
84
|
+
|
|
85
|
+
For example, `--run_until_records_in_org Account:50_000` means:
|
|
86
|
+
|
|
87
|
+
Count the Account records in the org. Let’s say the number
|
|
88
|
+
is 20,000. Thus, we must run the recipe over and
|
|
89
|
+
over again until we generate 30,000 new Account records.
|
|
90
|
+
If the recipe also generates e.g.Contacts, Opportunities or whatever
|
|
91
|
+
else, it generates the appropriate number of them to match.
|
|
92
|
+
|
|
93
|
+
Underscores are allowed but optional in big numbers: 2000000
|
|
94
|
+
is the same as 2_000_000.
|
|
95
|
+
"""
|
|
96
|
+
},
|
|
97
|
+
"run_until_records_loaded": {
|
|
98
|
+
"description": """<sobject>:<count>
|
|
99
|
+
|
|
100
|
+
Run the recipe repeatedly until the number of records of
|
|
101
|
+
<sobject> uploaded in this task execution matches <count>.
|
|
102
|
+
|
|
103
|
+
For example, `--run_until_records_loaded Account:50_000` means:
|
|
104
|
+
|
|
105
|
+
Run the recipe over and over again
|
|
106
|
+
until we generate 50_000 new Account records. If the recipe
|
|
107
|
+
also generates e.g. Contacts, Opportunities or whatever else, it
|
|
108
|
+
generates the appropriate number of them to match.
|
|
109
|
+
"""
|
|
110
|
+
},
|
|
111
|
+
"run_until_recipe_repeated": {
|
|
112
|
+
"description": """Run the recipe <count> times,
|
|
113
|
+
no matter what data is already in the org.
|
|
114
|
+
|
|
115
|
+
For example, `--run_until_recipe_repeated 50_000` means
|
|
116
|
+
run the recipe 50_000 times."""
|
|
117
|
+
},
|
|
118
|
+
"working_directory": {"description": "Path for temporary / working files"},
|
|
119
|
+
"loading_rules": {
|
|
120
|
+
"description": "Path to .load.yml file containing rules to use to "
|
|
121
|
+
"load the file. Defaults to `<recipename>.load.yml`. "
|
|
122
|
+
"Multiple files can be comma separated."
|
|
123
|
+
},
|
|
124
|
+
"recipe_options": {
|
|
125
|
+
"required": False,
|
|
126
|
+
"description": """Pass values to override options in the format VAR1:foo,VAR2:bar
|
|
127
|
+
|
|
128
|
+
Example: --recipe_options weight:10,color:purple""",
|
|
129
|
+
},
|
|
130
|
+
"bulk_mode": {
|
|
131
|
+
"description": "Set to Serial to serialize everything: data generation, data loading, data ingestion through bulk API. Parallel is the default."
|
|
132
|
+
},
|
|
133
|
+
"drop_missing_schema": {
|
|
134
|
+
"description": "Set to True to skip any missing objects or fields instead of stopping with an error."
|
|
135
|
+
},
|
|
136
|
+
"num_processes": {
|
|
137
|
+
"description": "Number of data generating processes. Defaults to matching the number of CPUs."
|
|
138
|
+
},
|
|
139
|
+
"ignore_row_errors": {
|
|
140
|
+
"description": "Boolean: should we continue loading even after running into row errors? "
|
|
141
|
+
"Defaults to False."
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def _validate_options(self):
|
|
146
|
+
"Validate options before executing the task or before freezing it"
|
|
147
|
+
super()._validate_options()
|
|
148
|
+
# Do not store recipe due to MetaDeploy options freezing
|
|
149
|
+
recipe = self.options.get("recipe")
|
|
150
|
+
recipe = Path(recipe)
|
|
151
|
+
if not recipe.exists():
|
|
152
|
+
raise exc.TaskOptionsError(f"Cannot find recipe `{recipe}`")
|
|
153
|
+
|
|
154
|
+
self.num_generator_workers = self.options.get("num_processes", None)
|
|
155
|
+
if self.num_generator_workers is not None:
|
|
156
|
+
self.num_generator_workers = int(self.num_generator_workers)
|
|
157
|
+
self.ignore_row_errors = process_bool_arg(
|
|
158
|
+
self.options.get("ignore_row_errors", False)
|
|
159
|
+
)
|
|
160
|
+
self.drop_missing_schema = process_bool_arg(
|
|
161
|
+
self.options.get("drop_missing_schema", False)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
loading_rules = process_list_arg(self.options.get("loading_rules")) or []
|
|
165
|
+
self.loading_rules = [Path(path) for path in loading_rules if path]
|
|
166
|
+
self.recipe_options = process_list_of_pairs_dict_arg(
|
|
167
|
+
self.options.get("recipe_options") or {}
|
|
168
|
+
)
|
|
169
|
+
self.bulk_mode = self.options.get("bulk_mode", "Parallel").title()
|
|
170
|
+
if self.bulk_mode and self.bulk_mode not in ["Serial", "Parallel"]:
|
|
171
|
+
raise TaskOptionsError("bulk_mode must be either Serial or Parallel")
|
|
172
|
+
|
|
173
|
+
def _init_channel_configs(self, recipe):
|
|
174
|
+
"""The channels describe the 'shape' of the communication
|
|
175
|
+
|
|
176
|
+
The normal case is a single, parallelized, bulk channel,
|
|
177
|
+
multi-threaded on client and server, using a single user
|
|
178
|
+
account.
|
|
179
|
+
|
|
180
|
+
Using .load.yml you can add more channels, utilizing
|
|
181
|
+
more user accounts which can speed up throughput in
|
|
182
|
+
a few cases.
|
|
183
|
+
|
|
184
|
+
This method reads files and options to determine
|
|
185
|
+
what channels should be created later.
|
|
186
|
+
"""
|
|
187
|
+
channel_decls = read_channel_declarations(recipe, self.loading_rules)
|
|
188
|
+
|
|
189
|
+
if channel_decls:
|
|
190
|
+
self.channel_configs = channel_configs_from_decls(
|
|
191
|
+
channel_decls, self.project_config.keychain
|
|
192
|
+
)
|
|
193
|
+
elif self.bulk_mode == "Serial":
|
|
194
|
+
self.channel_configs = [
|
|
195
|
+
standard_channel_config(
|
|
196
|
+
self.org_config,
|
|
197
|
+
self.recipe_options,
|
|
198
|
+
1,
|
|
199
|
+
1,
|
|
200
|
+
)
|
|
201
|
+
]
|
|
202
|
+
else:
|
|
203
|
+
self.channel_configs = [
|
|
204
|
+
standard_channel_config(
|
|
205
|
+
self.org_config,
|
|
206
|
+
self.recipe_options,
|
|
207
|
+
self.num_generator_workers,
|
|
208
|
+
None,
|
|
209
|
+
)
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
def setup(self):
|
|
213
|
+
"""Setup for loading."""
|
|
214
|
+
self.debug_mode = get_debug_mode()
|
|
215
|
+
if not self.num_generator_workers:
|
|
216
|
+
# logical CPUs do not really improve performance of CPU-bound
|
|
217
|
+
# code, so we ignore them.
|
|
218
|
+
self.num_generator_workers = psutil.cpu_count(logical=False)
|
|
219
|
+
if self.debug_mode:
|
|
220
|
+
self.logger.info(f"Using {self.num_generator_workers} workers")
|
|
221
|
+
|
|
222
|
+
self.run_until = determine_run_until(self.options, self.sf)
|
|
223
|
+
self.start_time = time.time()
|
|
224
|
+
self.recipe = Path(self.options.get("recipe"))
|
|
225
|
+
self.sobject_counts = defaultdict(RunningTotals)
|
|
226
|
+
self._init_channel_configs(self.recipe)
|
|
227
|
+
|
|
228
|
+
## Todo: Consider when this process runs longer than 2 Hours,
|
|
229
|
+
# what will happen to my sf connection?
|
|
230
|
+
def _run_task(self):
|
|
231
|
+
self.setup()
|
|
232
|
+
|
|
233
|
+
portions = PortionGenerator(
|
|
234
|
+
self.run_until.gap,
|
|
235
|
+
MIN_PORTION_SIZE,
|
|
236
|
+
MAX_PORTION_SIZE,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
working_directory = self.options.get("working_directory")
|
|
240
|
+
with self.workingdir_or_tempdir(working_directory) as working_directory:
|
|
241
|
+
self._setup_channels_and_queues(working_directory)
|
|
242
|
+
self.logger.info(f"Working directory is {working_directory}")
|
|
243
|
+
|
|
244
|
+
if self.run_until.nothing_to_do:
|
|
245
|
+
self.logger.info(
|
|
246
|
+
f"Dataload is finished before it started! {self.run_until.nothing_to_do_because}"
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
template_path, relevant_sobjects = self._generate_and_load_initial_batch(
|
|
251
|
+
working_directory
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# disable OrgReordCounts for now until it's reliability can be better
|
|
255
|
+
# tested and documented.
|
|
256
|
+
|
|
257
|
+
# Retrieve OrgRecordCounts code from
|
|
258
|
+
# https://github.com/SFDO-Tooling/CumulusCI/commit/7d703c44b94e8b21f165e5538c2249a65da0a9eb#diff-54676811961455410c30d9c9405a8f3b9d12a6222a58db9d55580a2da3cfb870R147
|
|
259
|
+
|
|
260
|
+
self._loop(
|
|
261
|
+
template_path,
|
|
262
|
+
working_directory,
|
|
263
|
+
None,
|
|
264
|
+
portions,
|
|
265
|
+
)
|
|
266
|
+
self.finish()
|
|
267
|
+
|
|
268
|
+
def _setup_channels_and_queues(self, working_directory):
|
|
269
|
+
"""Set up all of the channels and queues.
|
|
270
|
+
|
|
271
|
+
In particular their directories and the in-memory
|
|
272
|
+
runtime datastructures.
|
|
273
|
+
|
|
274
|
+
Each channel can hold multiple queues.
|
|
275
|
+
"""
|
|
276
|
+
additional_load_options = {
|
|
277
|
+
"ignore_row_errors": self.ignore_row_errors,
|
|
278
|
+
"drop_missing_schema": self.drop_missing_schema,
|
|
279
|
+
}
|
|
280
|
+
subtask_configurator = SubtaskConfigurator(
|
|
281
|
+
self.recipe, self.run_until, self.bulk_mode, additional_load_options
|
|
282
|
+
)
|
|
283
|
+
self.queue_manager = SnowfakeryChannelManager(
|
|
284
|
+
project_config=self.project_config,
|
|
285
|
+
logger=self.logger,
|
|
286
|
+
subtask_configurator=subtask_configurator,
|
|
287
|
+
)
|
|
288
|
+
if len(self.channel_configs) == 1:
|
|
289
|
+
channel = self.channel_configs[0]
|
|
290
|
+
self.queue_manager.add_channel(
|
|
291
|
+
org_config=channel.org_config,
|
|
292
|
+
num_generator_workers=channel.declaration.num_generators,
|
|
293
|
+
num_loader_workers=channel.declaration.num_loaders,
|
|
294
|
+
working_directory=working_directory,
|
|
295
|
+
recipe_options=channel.declaration.recipe_options,
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
self.configure_multiple_channels(working_directory)
|
|
299
|
+
|
|
300
|
+
def configure_multiple_channels(self, working_directory):
|
|
301
|
+
"""If there is more than one channel (=user account),
|
|
302
|
+
pre-allocate work among them.
|
|
303
|
+
"""
|
|
304
|
+
allocated_generator_workers = sum(
|
|
305
|
+
(channel.declaration.num_generators or 0)
|
|
306
|
+
for channel in self.channel_configs
|
|
307
|
+
)
|
|
308
|
+
channels_without_workers = len(
|
|
309
|
+
[
|
|
310
|
+
channel.declaration.num_generators
|
|
311
|
+
for channel in self.channel_configs
|
|
312
|
+
if not channel.declaration.num_generators
|
|
313
|
+
]
|
|
314
|
+
)
|
|
315
|
+
remaining_generator_workers = (
|
|
316
|
+
self.num_generator_workers - allocated_generator_workers
|
|
317
|
+
)
|
|
318
|
+
num_generators_per_channel = ceil(
|
|
319
|
+
remaining_generator_workers / channels_without_workers
|
|
320
|
+
)
|
|
321
|
+
for idx, channel in enumerate(self.channel_configs):
|
|
322
|
+
if self.debug_mode:
|
|
323
|
+
self.logger.info("Initializing %s", channel)
|
|
324
|
+
channel_wd = working_directory / f"channel_{idx}"
|
|
325
|
+
channel_wd.mkdir()
|
|
326
|
+
recipe_options = channel.merge_recipe_options(self.recipe_options)
|
|
327
|
+
generator_workers = (
|
|
328
|
+
channel.declaration.num_generators or num_generators_per_channel
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
self.queue_manager.add_channel(
|
|
332
|
+
org_config=channel.org_config,
|
|
333
|
+
num_generator_workers=generator_workers,
|
|
334
|
+
num_loader_workers=channel.declaration.num_loaders,
|
|
335
|
+
working_directory=channel_wd,
|
|
336
|
+
recipe_options=recipe_options,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _loop(
|
|
340
|
+
self,
|
|
341
|
+
template_path,
|
|
342
|
+
tempdir,
|
|
343
|
+
org_record_counts_thread,
|
|
344
|
+
portions: PortionGenerator,
|
|
345
|
+
):
|
|
346
|
+
"""The inner loop that controls when data is generated and when we are done."""
|
|
347
|
+
upload_status = self.get_upload_status(
|
|
348
|
+
portions.next_batch_size,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
while not portions.done(upload_status.total_sets_working_on_or_uploaded):
|
|
352
|
+
if self.debug_mode:
|
|
353
|
+
self.logger.info(f"Working Directory: {tempdir}")
|
|
354
|
+
|
|
355
|
+
self.queue_manager.tick(
|
|
356
|
+
upload_status,
|
|
357
|
+
template_path,
|
|
358
|
+
tempdir,
|
|
359
|
+
portions,
|
|
360
|
+
self.get_upload_status,
|
|
361
|
+
)
|
|
362
|
+
self.update_running_totals()
|
|
363
|
+
self.print_running_totals()
|
|
364
|
+
|
|
365
|
+
time.sleep(WAIT_TIME)
|
|
366
|
+
|
|
367
|
+
upload_status = self._report_status(
|
|
368
|
+
portions.batch_size,
|
|
369
|
+
org_record_counts_thread,
|
|
370
|
+
template_path,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return upload_status
|
|
374
|
+
|
|
375
|
+
def _report_status(
|
|
376
|
+
self,
|
|
377
|
+
batch_size,
|
|
378
|
+
org_record_counts_thread,
|
|
379
|
+
template_path,
|
|
380
|
+
):
|
|
381
|
+
"""Let the user know what is going on."""
|
|
382
|
+
self.logger.info(
|
|
383
|
+
"\n********** PROGRESS *********",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
upload_status = self.get_upload_status(
|
|
387
|
+
batch_size or 0,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
self.logger.info(upload_status._display(detailed=self.debug_mode))
|
|
391
|
+
|
|
392
|
+
if upload_status.sets_failed:
|
|
393
|
+
# TODO: this is not sufficiently tested.
|
|
394
|
+
# commenting it out doesn't break tests
|
|
395
|
+
self.log_failures()
|
|
396
|
+
|
|
397
|
+
if upload_status.sets_failed > ERROR_THRESHOLD:
|
|
398
|
+
raise exc.BulkDataException(
|
|
399
|
+
f"Errors exceeded threshold: {upload_status.sets_failed} vs {ERROR_THRESHOLD}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# TODO: Retrieve OrgRecordCounts code from
|
|
403
|
+
# https://github.com/SFDO-Tooling/CumulusCI/commit/7d703c44b94e8b21f165e5538c2249a65da0a9eb#diff-54676811961455410c30d9c9405a8f3b9d12a6222a58db9d55580a2da3cfb870R147
|
|
404
|
+
|
|
405
|
+
return upload_status
|
|
406
|
+
|
|
407
|
+
def update_running_totals(self) -> None:
|
|
408
|
+
"""Read and collate result reports from sub-processes/sub-threads
|
|
409
|
+
|
|
410
|
+
This is a realtime reporting channel which could, in theory, be updated
|
|
411
|
+
before sub-tasks finish. Currently no sub-tasks are coded to do that.
|
|
412
|
+
|
|
413
|
+
The logical next step is to allow LoadData to monitor steps one by
|
|
414
|
+
one or even batches one by one.
|
|
415
|
+
|
|
416
|
+
Note that until we implement that, we are paying the complexity
|
|
417
|
+
cost of a real-time channel but not getting the benefits of it.
|
|
418
|
+
"""
|
|
419
|
+
while True:
|
|
420
|
+
try:
|
|
421
|
+
results = self.queue_manager.get_results_report()
|
|
422
|
+
except Empty:
|
|
423
|
+
break
|
|
424
|
+
if "results" in results and "step_results" in results["results"]:
|
|
425
|
+
self.update_running_totals_from_load_step_results(results["results"])
|
|
426
|
+
elif "error" in results:
|
|
427
|
+
self.logger.warning(f"Error in load: {results}")
|
|
428
|
+
else: # pragma: no cover
|
|
429
|
+
self.logger.warning(f"Unexpected message from subtask: {results}")
|
|
430
|
+
|
|
431
|
+
def update_running_totals_from_load_step_results(self, results: dict) -> None:
|
|
432
|
+
"""'Parse' the results from a load step, to keep track of row errors."""
|
|
433
|
+
for result in results["step_results"].values():
|
|
434
|
+
sobject_name = result["sobject"]
|
|
435
|
+
totals = self.sobject_counts[sobject_name]
|
|
436
|
+
totals.errors += result["total_row_errors"]
|
|
437
|
+
totals.successes += result["records_processed"] - result["total_row_errors"]
|
|
438
|
+
|
|
439
|
+
def print_running_totals(self):
|
|
440
|
+
for name, result in self.sobject_counts.items():
|
|
441
|
+
self.logger.info(
|
|
442
|
+
f" {name}: {result.successes:,} successes, {result.errors:,} errors"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def finish(self):
|
|
446
|
+
"""Wait for jobs to finish"""
|
|
447
|
+
old_message = None
|
|
448
|
+
cooldown = 5
|
|
449
|
+
while not self.queue_manager.check_finished():
|
|
450
|
+
status = self.get_upload_status(0)
|
|
451
|
+
datagen_workers = f"{status.sets_being_generated} data generators, "
|
|
452
|
+
msg = f"Waiting for {datagen_workers}{status.sets_being_loaded} uploads to finish"
|
|
453
|
+
if old_message != msg or cooldown < 1:
|
|
454
|
+
old_message = msg
|
|
455
|
+
self.logger.info(msg)
|
|
456
|
+
self.update_running_totals()
|
|
457
|
+
self.print_running_totals()
|
|
458
|
+
cooldown = 5
|
|
459
|
+
else:
|
|
460
|
+
cooldown -= 1
|
|
461
|
+
time.sleep(WAIT_TIME)
|
|
462
|
+
|
|
463
|
+
self.log_failures()
|
|
464
|
+
|
|
465
|
+
self.logger.info("")
|
|
466
|
+
self.logger.info(" == Results == ")
|
|
467
|
+
self.update_running_totals()
|
|
468
|
+
self.print_running_totals()
|
|
469
|
+
elapsed = format_duration(timedelta(seconds=time.time() - self.start_time))
|
|
470
|
+
|
|
471
|
+
if self.run_until.sobject_name:
|
|
472
|
+
result_msg = f"{self.sobject_counts[self.run_until.sobject_name].successes} {self.run_until.sobject_name} records and associated records"
|
|
473
|
+
else:
|
|
474
|
+
result_msg = f"{self.run_until.target:,} iterations"
|
|
475
|
+
|
|
476
|
+
self.logger.info(f"☃ Snowfakery created {result_msg} in {elapsed}.")
|
|
477
|
+
|
|
478
|
+
def log_failures(self):
|
|
479
|
+
"""Log failures from sub-processes to main process"""
|
|
480
|
+
for exception in self.queue_manager.failure_descriptions():
|
|
481
|
+
self.logger.info(exception)
|
|
482
|
+
|
|
483
|
+
# TODO: This method is actually based on the number generated,
|
|
484
|
+
# because it is called before the load.
|
|
485
|
+
# If there are row errors, it will drift out of correctness
|
|
486
|
+
# Code needs to be updated to rename again after load.
|
|
487
|
+
# Or move away from using directory names for math altogether.
|
|
488
|
+
def data_loader_new_directory_name(self, working_dir: Path):
|
|
489
|
+
"""Change the directory name to reflect the true number of sets created."""
|
|
490
|
+
|
|
491
|
+
wd = SnowfakeryWorkingDirectory(working_dir)
|
|
492
|
+
key = wd.index
|
|
493
|
+
if key not in self.cached_counts:
|
|
494
|
+
self.cached_counts[key] = wd.get_record_counts()
|
|
495
|
+
|
|
496
|
+
if not self.run_until.sobject_name:
|
|
497
|
+
return working_dir
|
|
498
|
+
|
|
499
|
+
count = self.cached_counts[key][self.run_until.sobject_name]
|
|
500
|
+
|
|
501
|
+
path, _ = str(working_dir).rsplit("_", 1)
|
|
502
|
+
new_working_dir = Path(path + "_" + str(count))
|
|
503
|
+
return new_working_dir
|
|
504
|
+
|
|
505
|
+
def generator_data_dir(self, idx, template_path, batch_size, parent_dir):
|
|
506
|
+
"""Create a new generator directory with a name based on index and batch_size"""
|
|
507
|
+
assert batch_size > 0
|
|
508
|
+
data_dir = parent_dir / (str(idx) + "_" + str(batch_size))
|
|
509
|
+
shutil.copytree(template_path, data_dir)
|
|
510
|
+
return data_dir
|
|
511
|
+
|
|
512
|
+
def get_upload_status(
|
|
513
|
+
self,
|
|
514
|
+
batch_size,
|
|
515
|
+
):
|
|
516
|
+
"""Combine information from the different data sources into a single "report".
|
|
517
|
+
|
|
518
|
+
Useful for debugging but also for making decisions about what to do next."""
|
|
519
|
+
|
|
520
|
+
return self.queue_manager.get_upload_status(
|
|
521
|
+
batch_size, self.sets_finished_while_generating_template
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
@contextmanager
|
|
525
|
+
def workingdir_or_tempdir(self, working_directory: T.Optional[T.Union[Path, str]]):
|
|
526
|
+
"""Make a working directory or a temporary directory, as needed"""
|
|
527
|
+
if working_directory:
|
|
528
|
+
working_directory = Path(working_directory)
|
|
529
|
+
working_directory.mkdir()
|
|
530
|
+
self.logger.info(f"Working Directory {working_directory}")
|
|
531
|
+
yield working_directory
|
|
532
|
+
elif self.debug_mode:
|
|
533
|
+
working_directory = Path(mkdtemp())
|
|
534
|
+
self.logger.info(
|
|
535
|
+
f"Due to debug mode, Working Directory {working_directory} will not be removed"
|
|
536
|
+
)
|
|
537
|
+
yield working_directory
|
|
538
|
+
else:
|
|
539
|
+
with TemporaryDirectory() as tempdir:
|
|
540
|
+
yield Path(tempdir)
|
|
541
|
+
|
|
542
|
+
def _generate_and_load_initial_batch(self, working_directory: Path):
|
|
543
|
+
"""Generate a single batch to set up all just_once (singleton) objects"""
|
|
544
|
+
|
|
545
|
+
template_dir = Path(working_directory) / "template_1"
|
|
546
|
+
template_dir.mkdir()
|
|
547
|
+
# changes here should often be reflected in
|
|
548
|
+
# data_generator_opts and data_loader_opts
|
|
549
|
+
|
|
550
|
+
channel_decl = self.channel_configs[0]
|
|
551
|
+
|
|
552
|
+
plugin_options = {
|
|
553
|
+
"pid": "0",
|
|
554
|
+
"big_ids": "True",
|
|
555
|
+
}
|
|
556
|
+
# if it's efficient to do the whole load in one go, let's just do that.
|
|
557
|
+
if self.run_until.gap < MIN_PORTION_SIZE:
|
|
558
|
+
num_records = self.run_until.gap
|
|
559
|
+
else:
|
|
560
|
+
num_records = 1 # smallest possible batch to get to parallelizing fast
|
|
561
|
+
results = self._generate_and_load_batch(
|
|
562
|
+
template_dir,
|
|
563
|
+
channel_decl.org_config,
|
|
564
|
+
{
|
|
565
|
+
"generator_yaml": self.options.get("recipe"),
|
|
566
|
+
"num_records": num_records,
|
|
567
|
+
"num_records_tablename": self.run_until.sobject_name or COUNT_REPS,
|
|
568
|
+
"loading_rules": self.loading_rules,
|
|
569
|
+
"vars": channel_decl.merge_recipe_options(self.recipe_options),
|
|
570
|
+
"plugin_options": plugin_options,
|
|
571
|
+
"bulk_mode": self.bulk_mode,
|
|
572
|
+
},
|
|
573
|
+
)
|
|
574
|
+
self.update_running_totals_from_load_step_results(results)
|
|
575
|
+
|
|
576
|
+
# rename directory to reflect real number of sets created.
|
|
577
|
+
wd = SnowfakeryWorkingDirectory(template_dir)
|
|
578
|
+
if self.run_until.sobject_name:
|
|
579
|
+
self.sets_finished_while_generating_template = wd.get_record_counts()[
|
|
580
|
+
self.run_until.sobject_name
|
|
581
|
+
]
|
|
582
|
+
else:
|
|
583
|
+
self.sets_finished_while_generating_template = num_records
|
|
584
|
+
|
|
585
|
+
new_template_dir = data_loader_new_directory_name(template_dir, self.run_until)
|
|
586
|
+
# rename only if new_template_dir does not match template_dir
|
|
587
|
+
if template_dir.resolve() != new_template_dir.resolve():
|
|
588
|
+
shutil.move(template_dir, new_template_dir)
|
|
589
|
+
template_dir = new_template_dir
|
|
590
|
+
|
|
591
|
+
# don't send data tables to child processes. All they
|
|
592
|
+
# care about are ID->OID mappings
|
|
593
|
+
wd = SnowfakeryWorkingDirectory(template_dir)
|
|
594
|
+
self._cleanup_object_tables(*wd.setup_engine())
|
|
595
|
+
|
|
596
|
+
return template_dir, wd.relevant_sobjects()
|
|
597
|
+
|
|
598
|
+
def _generate_and_load_batch(self, tempdir, org_config, options) -> dict:
|
|
599
|
+
"""Before the "full" dataload starts we do a single batch to
|
|
600
|
+
load singletons.
|
|
601
|
+
"""
|
|
602
|
+
options = {
|
|
603
|
+
**options,
|
|
604
|
+
"working_directory": tempdir,
|
|
605
|
+
"set_recently_viewed": False,
|
|
606
|
+
"ignore_row_errors": self.ignore_row_errors,
|
|
607
|
+
"drop_missing_schema": self.drop_missing_schema,
|
|
608
|
+
}
|
|
609
|
+
subtask_config = TaskConfig({"options": options})
|
|
610
|
+
subtask = GenerateAndLoadDataFromYaml(
|
|
611
|
+
project_config=self.project_config,
|
|
612
|
+
task_config=subtask_config,
|
|
613
|
+
org_config=org_config,
|
|
614
|
+
flow=self.flow,
|
|
615
|
+
name=self.name,
|
|
616
|
+
stepnum=self.stepnum,
|
|
617
|
+
)
|
|
618
|
+
subtask()
|
|
619
|
+
return subtask.return_values["load_results"][0]
|
|
620
|
+
|
|
621
|
+
def _cleanup_object_tables(self, engine, metadata):
|
|
622
|
+
"""Delete all tables that do not relate to id->OID mapping"""
|
|
623
|
+
tables = metadata.tables
|
|
624
|
+
tables_to_drop = [
|
|
625
|
+
table
|
|
626
|
+
for tablename, table in tables.items()
|
|
627
|
+
if not tablename.endswith("sf_ids")
|
|
628
|
+
]
|
|
629
|
+
if tables_to_drop:
|
|
630
|
+
metadata.drop_all(tables=tables_to_drop)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class RunningTotals:
|
|
634
|
+
"""Keep track of # of row errors and successess"""
|
|
635
|
+
|
|
636
|
+
errors: int = 0
|
|
637
|
+
successes: int = 0
|
|
638
|
+
|
|
639
|
+
def __repr__(self):
|
|
640
|
+
return f"<{self.__class__.__name__} {self.__dict__}>"
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class RulesFileAndRules(T.NamedTuple):
|
|
644
|
+
file: Path
|
|
645
|
+
rules: T.List[ChannelDeclaration]
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _read_channel_declarations_from_file(
|
|
649
|
+
loading_rules_file: Path,
|
|
650
|
+
) -> RulesFileAndRules:
|
|
651
|
+
"""Look in a .load.yml file for the channel declarations"""
|
|
652
|
+
with loading_rules_file.open() as f:
|
|
653
|
+
decls = SObjectRuleDeclarationFile.parse_from_yaml(f)
|
|
654
|
+
channel_decls = decls.channel_declarations or []
|
|
655
|
+
assert isinstance(channel_decls, list)
|
|
656
|
+
return RulesFileAndRules(loading_rules_file, channel_decls)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def read_channel_declarations(
|
|
660
|
+
recipe: Path, loading_rules_files: T.List[Path]
|
|
661
|
+
) -> T.List[ChannelDeclaration]:
|
|
662
|
+
"""Find all appropriate channel declarations.
|
|
663
|
+
|
|
664
|
+
Some discovered through naming conventions, others
|
|
665
|
+
through command lines options or YML config."""
|
|
666
|
+
implicit_rules_file = infer_load_file_path(recipe)
|
|
667
|
+
if implicit_rules_file and implicit_rules_file.exists():
|
|
668
|
+
loading_rules_files = loading_rules_files + [implicit_rules_file]
|
|
669
|
+
# uniqify without losing order
|
|
670
|
+
loading_rules_files = list(dict.fromkeys(loading_rules_files))
|
|
671
|
+
|
|
672
|
+
rules_lists = [
|
|
673
|
+
_read_channel_declarations_from_file(file) for file in loading_rules_files
|
|
674
|
+
]
|
|
675
|
+
rules_lists = [rules_list for rules_list in rules_lists if rules_list.rules]
|
|
676
|
+
|
|
677
|
+
if len(rules_lists) > 1:
|
|
678
|
+
files = ", ".join(
|
|
679
|
+
[f"{rules_list.file}: {rules_list.rules}" for rules_list in rules_lists]
|
|
680
|
+
)
|
|
681
|
+
msg = f"Multiple channel declarations: {files}"
|
|
682
|
+
raise TaskOptionsError(msg)
|
|
683
|
+
elif len(rules_lists) == 1:
|
|
684
|
+
return rules_lists[0].rules
|
|
685
|
+
else:
|
|
686
|
+
return []
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class ChannelConfig(T.NamedTuple):
|
|
690
|
+
"""A channel represents a connection to Salesforce via a username.
|
|
691
|
+
|
|
692
|
+
It can also have recipe options and other documented properties.
|
|
693
|
+
https://github.com/SFDO-Tooling/Snowfakery/search?q=ChannelDeclaration
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
org_config: OrgConfig
|
|
697
|
+
declaration: ChannelDeclaration = None
|
|
698
|
+
|
|
699
|
+
def merge_recipe_options(self, task_recipe_options):
|
|
700
|
+
"""Merge the recipe options from the channel declaration with those from the task config"""
|
|
701
|
+
channel_options = self.declaration.recipe_options or {}
|
|
702
|
+
task_recipe_options = task_recipe_options or {}
|
|
703
|
+
self.check_conflicting_options(channel_options, task_recipe_options)
|
|
704
|
+
recipe_options = {
|
|
705
|
+
**task_recipe_options,
|
|
706
|
+
**channel_options,
|
|
707
|
+
}
|
|
708
|
+
return recipe_options
|
|
709
|
+
|
|
710
|
+
@staticmethod
|
|
711
|
+
def check_conflicting_options(channel_options, task_recipe_options):
|
|
712
|
+
"""Check that options do not conflict"""
|
|
713
|
+
double_specified_options = set(task_recipe_options.keys()).intersection(
|
|
714
|
+
set(channel_options.keys())
|
|
715
|
+
)
|
|
716
|
+
conflicting_options = [
|
|
717
|
+
optname
|
|
718
|
+
for optname in double_specified_options
|
|
719
|
+
if task_recipe_options[optname] != channel_options[optname]
|
|
720
|
+
]
|
|
721
|
+
if conflicting_options:
|
|
722
|
+
raise TaskOptionsError(
|
|
723
|
+
f"Recipe options cannot conflict: {conflicting_options}"
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def standard_channel_config(
|
|
728
|
+
org_config: OrgConfig,
|
|
729
|
+
recipe_options: dict,
|
|
730
|
+
num_generators: int,
|
|
731
|
+
num_loaders: int = None,
|
|
732
|
+
):
|
|
733
|
+
"""Default configuration for a single-channel data-load"""
|
|
734
|
+
channel = ChannelDeclaration(
|
|
735
|
+
user="Username not used in this context",
|
|
736
|
+
recipe_options=recipe_options,
|
|
737
|
+
num_generators=num_generators,
|
|
738
|
+
num_loaders=num_loaders,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
return ChannelConfig(org_config, channel)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def channel_configs_from_decls(
|
|
745
|
+
channel_decls: T.List[ChannelDeclaration],
|
|
746
|
+
keychain: BaseProjectKeychain,
|
|
747
|
+
):
|
|
748
|
+
"""Reify channel configs and look up orgconfig"""
|
|
749
|
+
|
|
750
|
+
def config_from_decl(decl):
|
|
751
|
+
return ChannelConfig(keychain.get_org(decl.user), decl)
|
|
752
|
+
|
|
753
|
+
return [config_from_decl(decl) for decl in channel_decls]
|