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,792 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import random
|
|
3
|
+
import re
|
|
4
|
+
import typing as T
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, root_validator, validator
|
|
8
|
+
|
|
9
|
+
from cumulusci.core.enums import StrEnum
|
|
10
|
+
from cumulusci.tasks.bulkdata.utils import CaseInsensitiveDict
|
|
11
|
+
from cumulusci.utils import get_cci_upgrade_command
|
|
12
|
+
from cumulusci.utils.yaml.model_parser import CCIDictModel
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
try:
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
from annoy import AnnoyIndex
|
|
19
|
+
from sklearn.feature_extraction.text import HashingVectorizer
|
|
20
|
+
from sklearn.preprocessing import StandardScaler
|
|
21
|
+
|
|
22
|
+
OPTIONAL_DEPENDENCIES_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
logger.warning(
|
|
25
|
+
f"Optional dependencies are missing. "
|
|
26
|
+
"Handling high volumes of records for the 'select' functionality will be significantly slower, "
|
|
27
|
+
"as optimizations for this feature are currently disabled. "
|
|
28
|
+
f"To enable optimized performance, install all required dependencies using: {get_cci_upgrade_command()}[select]\n"
|
|
29
|
+
)
|
|
30
|
+
OPTIONAL_DEPENDENCIES_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SelectStrategy(StrEnum):
|
|
34
|
+
"""Enum defining the different selection strategies requested."""
|
|
35
|
+
|
|
36
|
+
STANDARD = "standard"
|
|
37
|
+
SIMILARITY = "similarity"
|
|
38
|
+
RANDOM = "random"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SelectRecordRetrievalMode(StrEnum):
|
|
42
|
+
"""Enum defining whether you need all records or match the
|
|
43
|
+
number of records of the local sql file"""
|
|
44
|
+
|
|
45
|
+
ALL = "all"
|
|
46
|
+
MATCH = "match"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
ENUM_VALUES = {
|
|
50
|
+
v.value.lower(): v.value
|
|
51
|
+
for enum in [SelectStrategy]
|
|
52
|
+
for v in enum.__members__.values()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SelectOptions(CCIDictModel):
|
|
57
|
+
filter: T.Optional[str] = None # Optional filter for selection
|
|
58
|
+
strategy: SelectStrategy = SelectStrategy.STANDARD # Strategy for selection
|
|
59
|
+
priority_fields: T.Dict[str, str] = Field({})
|
|
60
|
+
threshold: T.Optional[float] = None
|
|
61
|
+
|
|
62
|
+
@validator("strategy", pre=True)
|
|
63
|
+
def validate_strategy(cls, value):
|
|
64
|
+
if isinstance(value, Enum):
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
if value:
|
|
68
|
+
matched_strategy = ENUM_VALUES.get(value.lower())
|
|
69
|
+
if matched_strategy:
|
|
70
|
+
return matched_strategy
|
|
71
|
+
|
|
72
|
+
raise ValueError(f"Invalid strategy value: {value}")
|
|
73
|
+
|
|
74
|
+
@validator("priority_fields", pre=True)
|
|
75
|
+
def standardize_fields_to_dict(cls, values):
|
|
76
|
+
if values is None:
|
|
77
|
+
values = {}
|
|
78
|
+
if type(values) is list:
|
|
79
|
+
values = {elem: elem for elem in values}
|
|
80
|
+
return CaseInsensitiveDict(values)
|
|
81
|
+
|
|
82
|
+
@root_validator
|
|
83
|
+
def validate_threshold_and_strategy(cls, values):
|
|
84
|
+
threshold = values.get("threshold")
|
|
85
|
+
strategy = values.get("strategy")
|
|
86
|
+
|
|
87
|
+
if threshold is not None:
|
|
88
|
+
values["threshold"] = float(threshold) # Convert to float
|
|
89
|
+
|
|
90
|
+
if not (0 <= values["threshold"] <= 1):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Threshold must be between 0 and 1, got {values['threshold']}."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if strategy != SelectStrategy.SIMILARITY:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"If a threshold is specified, the strategy must be set to 'similarity'."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return values
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SelectOperationExecutor:
|
|
104
|
+
def __init__(self, strategy: SelectStrategy):
|
|
105
|
+
self.strategy = strategy
|
|
106
|
+
self.retrieval_mode = (
|
|
107
|
+
SelectRecordRetrievalMode.ALL
|
|
108
|
+
if strategy == SelectStrategy.SIMILARITY
|
|
109
|
+
else SelectRecordRetrievalMode.MATCH
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def select_generate_query(
|
|
113
|
+
self,
|
|
114
|
+
sobject: str,
|
|
115
|
+
fields: T.List[str],
|
|
116
|
+
user_filter: str,
|
|
117
|
+
limit: T.Union[int, None],
|
|
118
|
+
offset: T.Union[int, None],
|
|
119
|
+
):
|
|
120
|
+
_, select_fields = split_and_filter_fields(fields=fields)
|
|
121
|
+
# For STANDARD strategy
|
|
122
|
+
if self.strategy == SelectStrategy.STANDARD:
|
|
123
|
+
return standard_generate_query(
|
|
124
|
+
sobject=sobject, user_filter=user_filter, limit=limit, offset=offset
|
|
125
|
+
)
|
|
126
|
+
# For SIMILARITY strategy
|
|
127
|
+
elif self.strategy == SelectStrategy.SIMILARITY:
|
|
128
|
+
return similarity_generate_query(
|
|
129
|
+
sobject=sobject,
|
|
130
|
+
fields=select_fields,
|
|
131
|
+
user_filter=user_filter,
|
|
132
|
+
limit=limit,
|
|
133
|
+
offset=offset,
|
|
134
|
+
)
|
|
135
|
+
# For RANDOM strategy
|
|
136
|
+
elif self.strategy == SelectStrategy.RANDOM:
|
|
137
|
+
return standard_generate_query(
|
|
138
|
+
sobject=sobject, user_filter=user_filter, limit=limit, offset=offset
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def select_post_process(
|
|
142
|
+
self,
|
|
143
|
+
load_records,
|
|
144
|
+
query_records: list,
|
|
145
|
+
fields: list,
|
|
146
|
+
num_records: int,
|
|
147
|
+
sobject: str,
|
|
148
|
+
weights: list,
|
|
149
|
+
threshold: T.Union[float, None],
|
|
150
|
+
):
|
|
151
|
+
# For STANDARD strategy
|
|
152
|
+
if self.strategy == SelectStrategy.STANDARD:
|
|
153
|
+
return standard_post_process(
|
|
154
|
+
query_records=query_records, num_records=num_records, sobject=sobject
|
|
155
|
+
)
|
|
156
|
+
# For SIMILARITY strategy
|
|
157
|
+
elif self.strategy == SelectStrategy.SIMILARITY:
|
|
158
|
+
return similarity_post_process(
|
|
159
|
+
load_records=load_records,
|
|
160
|
+
query_records=query_records,
|
|
161
|
+
fields=fields,
|
|
162
|
+
sobject=sobject,
|
|
163
|
+
weights=weights,
|
|
164
|
+
threshold=threshold,
|
|
165
|
+
)
|
|
166
|
+
# For RANDOM strategy
|
|
167
|
+
elif self.strategy == SelectStrategy.RANDOM:
|
|
168
|
+
return random_post_process(
|
|
169
|
+
query_records=query_records, num_records=num_records, sobject=sobject
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def standard_generate_query(
|
|
174
|
+
sobject: str,
|
|
175
|
+
user_filter: str,
|
|
176
|
+
limit: T.Union[int, None],
|
|
177
|
+
offset: T.Union[int, None],
|
|
178
|
+
) -> T.Tuple[str, T.List[str]]:
|
|
179
|
+
"""Generates the SOQL query for the standard (as well as random) selection strategy"""
|
|
180
|
+
|
|
181
|
+
query = f"SELECT Id FROM {sobject}"
|
|
182
|
+
# If user specifies user_filter
|
|
183
|
+
if user_filter:
|
|
184
|
+
query += add_limit_offset_to_user_filter(
|
|
185
|
+
filter_clause=user_filter, limit_clause=limit, offset_clause=offset
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
query += f" LIMIT {limit}" if limit else ""
|
|
189
|
+
query += f" OFFSET {offset}" if offset else ""
|
|
190
|
+
return query, ["Id"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def standard_post_process(
|
|
194
|
+
query_records: list, num_records: int, sobject: str
|
|
195
|
+
) -> T.Tuple[T.List[dict], None, T.Union[str, None]]:
|
|
196
|
+
"""Processes the query results for the standard selection strategy"""
|
|
197
|
+
# Handle case where query returns 0 records
|
|
198
|
+
if not query_records:
|
|
199
|
+
error_message = f"No records found for {sobject} in the target org."
|
|
200
|
+
return [], None, error_message
|
|
201
|
+
|
|
202
|
+
# Add 'success: True' to each record to emulate records have been inserted
|
|
203
|
+
selected_records = [
|
|
204
|
+
{"id": record[0], "success": True, "created": False} for record in query_records
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
# If fewer records than requested, repeat existing records to match num_records
|
|
208
|
+
if len(selected_records) < num_records:
|
|
209
|
+
original_records = selected_records.copy()
|
|
210
|
+
while len(selected_records) < num_records:
|
|
211
|
+
selected_records.extend(original_records)
|
|
212
|
+
selected_records = selected_records[:num_records]
|
|
213
|
+
|
|
214
|
+
return selected_records, None, None # Return selected records and None for error
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def similarity_generate_query(
|
|
218
|
+
sobject: str,
|
|
219
|
+
fields: T.List[str],
|
|
220
|
+
user_filter: str,
|
|
221
|
+
limit: T.Union[int, None],
|
|
222
|
+
offset: T.Union[int, None],
|
|
223
|
+
) -> T.Tuple[str, T.List[str]]:
|
|
224
|
+
"""Generates the SOQL query for the similarity selection strategy, with support for TYPEOF on polymorphic fields."""
|
|
225
|
+
|
|
226
|
+
# Pre-process the new fields format to create a nested dict structure for TYPEOF clauses
|
|
227
|
+
nested_fields = {}
|
|
228
|
+
regular_fields = []
|
|
229
|
+
|
|
230
|
+
for field in fields:
|
|
231
|
+
components = field.split(".")
|
|
232
|
+
if len(components) >= 3:
|
|
233
|
+
# Handle polymorphic fields (format: {relationship_name}.{ref_obj}.{ref_field})
|
|
234
|
+
relationship, ref_obj, ref_field = (
|
|
235
|
+
components[0],
|
|
236
|
+
components[1],
|
|
237
|
+
components[2],
|
|
238
|
+
)
|
|
239
|
+
if relationship not in nested_fields:
|
|
240
|
+
nested_fields[relationship] = {}
|
|
241
|
+
if ref_obj not in nested_fields[relationship]:
|
|
242
|
+
nested_fields[relationship][ref_obj] = []
|
|
243
|
+
nested_fields[relationship][ref_obj].append(ref_field)
|
|
244
|
+
else:
|
|
245
|
+
# Handle regular fields (format: {field})
|
|
246
|
+
regular_fields.append(field)
|
|
247
|
+
|
|
248
|
+
# Construct the query fields
|
|
249
|
+
query_fields = []
|
|
250
|
+
|
|
251
|
+
# Build TYPEOF clauses for polymorphic fields
|
|
252
|
+
for relationship, references in nested_fields.items():
|
|
253
|
+
type_clauses = []
|
|
254
|
+
for ref_obj, ref_fields in references.items():
|
|
255
|
+
fields_clause = ", ".join(ref_fields)
|
|
256
|
+
type_clauses.append(f"WHEN {ref_obj} THEN {fields_clause}")
|
|
257
|
+
type_clause = f"TYPEOF {relationship} {' '.join(type_clauses)} ELSE Id END"
|
|
258
|
+
query_fields.append(type_clause)
|
|
259
|
+
|
|
260
|
+
# Add regular fields to the query
|
|
261
|
+
query_fields.extend(regular_fields)
|
|
262
|
+
|
|
263
|
+
# Ensure "Id" is included in the fields list for identification
|
|
264
|
+
if "Id" not in query_fields:
|
|
265
|
+
query_fields.insert(0, "Id")
|
|
266
|
+
|
|
267
|
+
# Build the main SOQL query
|
|
268
|
+
fields_to_query = ", ".join(query_fields)
|
|
269
|
+
query = f"SELECT {fields_to_query} FROM {sobject}"
|
|
270
|
+
|
|
271
|
+
# Add the user-defined filter clause or default clause
|
|
272
|
+
if user_filter:
|
|
273
|
+
query += add_limit_offset_to_user_filter(
|
|
274
|
+
filter_clause=user_filter, limit_clause=limit, offset_clause=offset
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
query += f" LIMIT {limit}" if limit else ""
|
|
278
|
+
query += f" OFFSET {offset}" if offset else ""
|
|
279
|
+
|
|
280
|
+
# Return the original input fields with "Id" added if needed
|
|
281
|
+
if "Id" not in fields:
|
|
282
|
+
fields.insert(0, "Id")
|
|
283
|
+
|
|
284
|
+
return query, fields
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def similarity_post_process(
|
|
288
|
+
load_records,
|
|
289
|
+
query_records: list,
|
|
290
|
+
fields: list,
|
|
291
|
+
sobject: str,
|
|
292
|
+
weights: list,
|
|
293
|
+
threshold: T.Union[float, None],
|
|
294
|
+
) -> T.Tuple[
|
|
295
|
+
T.List[T.Union[dict, None]], T.List[T.Union[list, None]], T.Union[str, None]
|
|
296
|
+
]:
|
|
297
|
+
"""Processes the query results for the similarity selection strategy"""
|
|
298
|
+
# Handle case where query returns 0 records
|
|
299
|
+
if not query_records and threshold is None:
|
|
300
|
+
error_message = f"No records found for {sobject} in the target org."
|
|
301
|
+
return [], [], error_message
|
|
302
|
+
|
|
303
|
+
load_records = list(load_records)
|
|
304
|
+
# Replace None values in each row with empty strings
|
|
305
|
+
for idx, row in enumerate(load_records):
|
|
306
|
+
row = [value if value is not None else "" for value in row]
|
|
307
|
+
load_records[idx] = row
|
|
308
|
+
load_record_count, query_record_count = len(load_records), len(query_records)
|
|
309
|
+
|
|
310
|
+
complexity_constant = load_record_count * query_record_count
|
|
311
|
+
|
|
312
|
+
select_records = []
|
|
313
|
+
insert_records = []
|
|
314
|
+
|
|
315
|
+
if complexity_constant < 1000 or not OPTIONAL_DEPENDENCIES_AVAILABLE:
|
|
316
|
+
select_records, insert_records = levenshtein_post_process(
|
|
317
|
+
load_records, query_records, fields, weights, threshold
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
select_records, insert_records = annoy_post_process(
|
|
321
|
+
load_records, query_records, fields, weights, threshold
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return select_records, insert_records, None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def annoy_post_process(
|
|
328
|
+
load_records: list,
|
|
329
|
+
query_records: list,
|
|
330
|
+
all_fields: list,
|
|
331
|
+
similarity_weights: list,
|
|
332
|
+
threshold: T.Union[float, None],
|
|
333
|
+
) -> T.Tuple[T.List[dict], list]:
|
|
334
|
+
"""Processes the query results for the similarity selection strategy using Annoy algorithm for large number of records"""
|
|
335
|
+
# Add warning when threshold is 0
|
|
336
|
+
if threshold is not None and threshold == 0:
|
|
337
|
+
logger.warning(
|
|
338
|
+
"Warning: A threshold of 0 may miss exact matches in high volumes. Use a small value like 0.1 for better accuracy."
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
selected_records = []
|
|
342
|
+
insertion_candidates = []
|
|
343
|
+
|
|
344
|
+
# Split fields into load and select categories
|
|
345
|
+
load_field_list, select_field_list = split_and_filter_fields(fields=all_fields)
|
|
346
|
+
# Only select those weights for select field list
|
|
347
|
+
similarity_weights = [
|
|
348
|
+
similarity_weights[idx]
|
|
349
|
+
for idx, field in enumerate(all_fields)
|
|
350
|
+
if field in select_field_list
|
|
351
|
+
]
|
|
352
|
+
load_shaped_records = reorder_records(
|
|
353
|
+
records=load_records, original_fields=all_fields, new_fields=load_field_list
|
|
354
|
+
)
|
|
355
|
+
select_shaped_records = reorder_records(
|
|
356
|
+
records=load_records, original_fields=all_fields, new_fields=select_field_list
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if not query_records:
|
|
360
|
+
# Directly append to load record for insertion if target_records is empty
|
|
361
|
+
selected_records = [None for _ in load_records]
|
|
362
|
+
insertion_candidates = load_shaped_records
|
|
363
|
+
return selected_records, insertion_candidates
|
|
364
|
+
|
|
365
|
+
hash_features = 100
|
|
366
|
+
# Adjust number of trees for small datasets to avoid issues
|
|
367
|
+
num_trees = max(1, min(10, len(query_records)))
|
|
368
|
+
|
|
369
|
+
query_record_ids = [record[0] for record in query_records]
|
|
370
|
+
query_record_data = [record[1:] for record in query_records]
|
|
371
|
+
|
|
372
|
+
record_to_id_map = {
|
|
373
|
+
tuple(query_record_data[i]): query_record_ids[i]
|
|
374
|
+
for i in range(len(query_records))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
final_load_vectors, final_query_vectors = vectorize_records(
|
|
378
|
+
select_shaped_records,
|
|
379
|
+
query_record_data,
|
|
380
|
+
hash_features=hash_features,
|
|
381
|
+
weights=similarity_weights,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Create Annoy index for nearest neighbor search
|
|
385
|
+
vector_dimension = final_query_vectors.shape[1]
|
|
386
|
+
annoy_index = AnnoyIndex(vector_dimension, "euclidean")
|
|
387
|
+
|
|
388
|
+
for i in range(len(final_query_vectors)):
|
|
389
|
+
annoy_index.add_item(i, final_query_vectors[i])
|
|
390
|
+
|
|
391
|
+
# Build the index
|
|
392
|
+
annoy_index.build(num_trees)
|
|
393
|
+
|
|
394
|
+
# Find nearest neighbors for each query vector
|
|
395
|
+
# For small datasets, search more neighbors to ensure we find the best match
|
|
396
|
+
n_neighbors = min(len(query_records), 2)
|
|
397
|
+
|
|
398
|
+
for i, load_vector in enumerate(final_load_vectors):
|
|
399
|
+
# Get nearest neighbors' indices and distances
|
|
400
|
+
nearest_neighbors = annoy_index.get_nns_by_vector(
|
|
401
|
+
load_vector, n_neighbors, include_distances=True
|
|
402
|
+
)
|
|
403
|
+
neighbor_indices = nearest_neighbors[0] # Indices of nearest neighbors
|
|
404
|
+
neighbor_distances = [
|
|
405
|
+
distance / 2 for distance in nearest_neighbors[1]
|
|
406
|
+
] # Distances sqrt(2(1-cos(u,v)))/2 lies between [0,1]
|
|
407
|
+
|
|
408
|
+
# Find the best match (minimum distance)
|
|
409
|
+
best_distance = neighbor_distances[0]
|
|
410
|
+
best_neighbor_index = neighbor_indices[0]
|
|
411
|
+
|
|
412
|
+
for idx in range(1, len(neighbor_indices)):
|
|
413
|
+
if neighbor_distances[idx] < best_distance:
|
|
414
|
+
best_distance = neighbor_distances[idx]
|
|
415
|
+
best_neighbor_index = neighbor_indices[idx]
|
|
416
|
+
|
|
417
|
+
# Use the best match
|
|
418
|
+
record = query_record_data[best_neighbor_index]
|
|
419
|
+
closest_record_id = record_to_id_map[tuple(record)]
|
|
420
|
+
if threshold is not None and (best_distance >= threshold):
|
|
421
|
+
selected_records.append(None)
|
|
422
|
+
insertion_candidates.append(load_shaped_records[i])
|
|
423
|
+
else:
|
|
424
|
+
selected_records.append(
|
|
425
|
+
{"id": closest_record_id, "success": True, "created": False}
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return selected_records, insertion_candidates
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def levenshtein_post_process(
|
|
432
|
+
source_records: list,
|
|
433
|
+
target_records: list,
|
|
434
|
+
all_fields: list,
|
|
435
|
+
similarity_weights: list,
|
|
436
|
+
distance_threshold: T.Union[float, None],
|
|
437
|
+
) -> T.Tuple[T.List[T.Optional[dict]], T.List[T.Optional[list]]]:
|
|
438
|
+
"""Processes query results using Levenshtein algorithm for similarity selection with a small number of records."""
|
|
439
|
+
selected_records = []
|
|
440
|
+
insertion_candidates = []
|
|
441
|
+
|
|
442
|
+
# Split fields into load and select categories
|
|
443
|
+
load_field_list, select_field_list = split_and_filter_fields(fields=all_fields)
|
|
444
|
+
# Only select those weights for select field list
|
|
445
|
+
similarity_weights = [
|
|
446
|
+
similarity_weights[idx]
|
|
447
|
+
for idx, field in enumerate(all_fields)
|
|
448
|
+
if field in select_field_list
|
|
449
|
+
]
|
|
450
|
+
load_shaped_records = reorder_records(
|
|
451
|
+
records=source_records, original_fields=all_fields, new_fields=load_field_list
|
|
452
|
+
)
|
|
453
|
+
select_shaped_records = reorder_records(
|
|
454
|
+
records=source_records, original_fields=all_fields, new_fields=select_field_list
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if not target_records:
|
|
458
|
+
# Directly append to load record for insertion if target_records is empty
|
|
459
|
+
selected_records = [None for _ in source_records]
|
|
460
|
+
insertion_candidates = load_shaped_records
|
|
461
|
+
return selected_records, insertion_candidates
|
|
462
|
+
|
|
463
|
+
for select_record, load_record in zip(select_shaped_records, load_shaped_records):
|
|
464
|
+
closest_match, match_distance = find_closest_record(
|
|
465
|
+
select_record, target_records, similarity_weights
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if distance_threshold is not None and match_distance > distance_threshold:
|
|
469
|
+
# Append load record for insertion if distance exceeds threshold
|
|
470
|
+
insertion_candidates.append(load_record)
|
|
471
|
+
selected_records.append(None)
|
|
472
|
+
elif closest_match:
|
|
473
|
+
# Append match details if distance is within threshold
|
|
474
|
+
selected_records.append(
|
|
475
|
+
{"id": closest_match[0], "success": True, "created": False}
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return selected_records, insertion_candidates
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def random_post_process(
|
|
482
|
+
query_records: list, num_records: int, sobject: str
|
|
483
|
+
) -> T.Tuple[T.List[dict], None, T.Union[str, None]]:
|
|
484
|
+
"""Processes the query results for the random selection strategy"""
|
|
485
|
+
|
|
486
|
+
if not query_records:
|
|
487
|
+
error_message = f"No records found for {sobject} in the target org."
|
|
488
|
+
return [], None, error_message
|
|
489
|
+
|
|
490
|
+
selected_records = []
|
|
491
|
+
for _ in range(num_records): # Loop 'num_records' times
|
|
492
|
+
# Randomly select one record from query_records
|
|
493
|
+
random_record = random.choice(query_records)
|
|
494
|
+
selected_records.append(
|
|
495
|
+
{"id": random_record[0], "success": True, "created": False}
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return selected_records, None, None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def find_closest_record(load_record: list, query_records: list, weights: list):
|
|
502
|
+
closest_distance = float("inf")
|
|
503
|
+
closest_record = query_records[0]
|
|
504
|
+
|
|
505
|
+
for record in query_records:
|
|
506
|
+
distance = calculate_levenshtein_distance(load_record, record[1:], weights)
|
|
507
|
+
if distance < closest_distance:
|
|
508
|
+
closest_distance = distance
|
|
509
|
+
closest_record = record
|
|
510
|
+
|
|
511
|
+
return closest_record, closest_distance
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def levenshtein_distance(str1: str, str2: str):
|
|
515
|
+
"""Calculate the Levenshtein distance between two strings"""
|
|
516
|
+
len_str1 = len(str1) + 1
|
|
517
|
+
len_str2 = len(str2) + 1
|
|
518
|
+
|
|
519
|
+
dp = [[0 for _ in range(len_str2)] for _ in range(len_str1)]
|
|
520
|
+
|
|
521
|
+
for i in range(len_str1):
|
|
522
|
+
dp[i][0] = i
|
|
523
|
+
for j in range(len_str2):
|
|
524
|
+
dp[0][j] = j
|
|
525
|
+
|
|
526
|
+
for i in range(1, len_str1):
|
|
527
|
+
for j in range(1, len_str2):
|
|
528
|
+
cost = 0 if str1[i - 1] == str2[j - 1] else 1
|
|
529
|
+
dp[i][j] = min(
|
|
530
|
+
dp[i - 1][j] + 1, # Deletion
|
|
531
|
+
dp[i][j - 1] + 1, # Insertion
|
|
532
|
+
dp[i - 1][j - 1] + cost,
|
|
533
|
+
) # Substitution
|
|
534
|
+
|
|
535
|
+
return dp[-1][-1]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def calculate_levenshtein_distance(record1: list, record2: list, weights: list):
|
|
539
|
+
if len(record1) != len(record2):
|
|
540
|
+
raise ValueError("Records must have the same number of fields.")
|
|
541
|
+
elif len(record1) != len(weights):
|
|
542
|
+
raise ValueError("Records must be same size as fields (weights).")
|
|
543
|
+
|
|
544
|
+
total_distance = 0
|
|
545
|
+
|
|
546
|
+
for field1, field2, weight in zip(record1, record2, weights):
|
|
547
|
+
field1 = field1.lower()
|
|
548
|
+
field2 = field2.lower()
|
|
549
|
+
|
|
550
|
+
if len(field1) == 0 and len(field2) == 0:
|
|
551
|
+
# If both fields are blank, distance is 0
|
|
552
|
+
distance = 0
|
|
553
|
+
else:
|
|
554
|
+
# Average distance per character
|
|
555
|
+
distance = levenshtein_distance(field1, field2) / max(
|
|
556
|
+
len(field1), len(field2)
|
|
557
|
+
)
|
|
558
|
+
if len(field1) == 0 or len(field2) == 0:
|
|
559
|
+
# If one field is blank, reduce the impact of the distance
|
|
560
|
+
distance = distance * 0.05 # Fixed value for blank vs non-blank
|
|
561
|
+
|
|
562
|
+
# Multiply the distance by the corresponding weight
|
|
563
|
+
total_distance += distance * weight
|
|
564
|
+
|
|
565
|
+
# Average distance per character with weights
|
|
566
|
+
return total_distance / sum(weights) if len(weights) else 0
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def add_limit_offset_to_user_filter(
|
|
570
|
+
filter_clause: str,
|
|
571
|
+
limit_clause: T.Union[float, None] = None,
|
|
572
|
+
offset_clause: T.Union[float, None] = None,
|
|
573
|
+
) -> str:
|
|
574
|
+
|
|
575
|
+
# Extract existing LIMIT and OFFSET from filter_clause if present
|
|
576
|
+
existing_limit_match = re.search(r"LIMIT\s+(\d+)", filter_clause, re.IGNORECASE)
|
|
577
|
+
existing_offset_match = re.search(r"OFFSET\s+(\d+)", filter_clause, re.IGNORECASE)
|
|
578
|
+
|
|
579
|
+
if existing_limit_match:
|
|
580
|
+
existing_limit = int(existing_limit_match.group(1))
|
|
581
|
+
if limit_clause is not None: # Only apply limit_clause if it's provided
|
|
582
|
+
limit_clause = min(existing_limit, limit_clause)
|
|
583
|
+
else:
|
|
584
|
+
limit_clause = existing_limit
|
|
585
|
+
|
|
586
|
+
if existing_offset_match:
|
|
587
|
+
existing_offset = int(existing_offset_match.group(1))
|
|
588
|
+
if offset_clause is not None:
|
|
589
|
+
offset_clause = existing_offset + offset_clause
|
|
590
|
+
else:
|
|
591
|
+
offset_clause = existing_offset
|
|
592
|
+
|
|
593
|
+
# Remove existing LIMIT and OFFSET from filter_clause, handling potential extra spaces
|
|
594
|
+
filter_clause = re.sub(
|
|
595
|
+
r"\s+OFFSET\s+\d+\s*", " ", filter_clause, flags=re.IGNORECASE
|
|
596
|
+
).strip()
|
|
597
|
+
filter_clause = re.sub(
|
|
598
|
+
r"\s+LIMIT\s+\d+\s*", " ", filter_clause, flags=re.IGNORECASE
|
|
599
|
+
).strip()
|
|
600
|
+
|
|
601
|
+
if limit_clause is not None:
|
|
602
|
+
filter_clause += f" LIMIT {limit_clause}"
|
|
603
|
+
if offset_clause is not None:
|
|
604
|
+
filter_clause += f" OFFSET {offset_clause}"
|
|
605
|
+
|
|
606
|
+
return f" {filter_clause}"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def determine_field_types(df_db, df_query, weights):
|
|
610
|
+
numerical_features = []
|
|
611
|
+
boolean_features = []
|
|
612
|
+
categorical_features = []
|
|
613
|
+
|
|
614
|
+
numerical_weights = []
|
|
615
|
+
boolean_weights = []
|
|
616
|
+
categorical_weights = []
|
|
617
|
+
|
|
618
|
+
for col, weight in zip(df_db.columns, weights):
|
|
619
|
+
# Check if the column can be converted to numeric
|
|
620
|
+
try:
|
|
621
|
+
temp_df_db = pd.to_numeric(df_db[col], errors="raise")
|
|
622
|
+
temp_df_query = pd.to_numeric(df_query[col], errors="raise")
|
|
623
|
+
# Replace empty values with 0 for numerical features
|
|
624
|
+
df_db[col] = temp_df_db.fillna(0).replace("", 0)
|
|
625
|
+
df_query[col] = temp_df_query.fillna(0).replace("", 0)
|
|
626
|
+
numerical_features.append(col)
|
|
627
|
+
numerical_weights.append(weight)
|
|
628
|
+
except ValueError:
|
|
629
|
+
# Check for boolean values
|
|
630
|
+
if (
|
|
631
|
+
df_db[col].str.lower().isin(["true", "false"]).all()
|
|
632
|
+
and df_query[col].str.lower().isin(["true", "false"]).all()
|
|
633
|
+
):
|
|
634
|
+
# Map to actual boolean values
|
|
635
|
+
df_db[col] = df_db[col].str.lower().map({"true": True, "false": False})
|
|
636
|
+
df_query[col] = (
|
|
637
|
+
df_query[col].str.lower().map({"true": True, "false": False})
|
|
638
|
+
)
|
|
639
|
+
boolean_features.append(col)
|
|
640
|
+
boolean_weights.append(weight)
|
|
641
|
+
else:
|
|
642
|
+
categorical_features.append(col)
|
|
643
|
+
categorical_weights.append(weight)
|
|
644
|
+
# Replace empty values with 'missing' for categorical features
|
|
645
|
+
df_db[col] = df_db[col].replace("", "missing")
|
|
646
|
+
df_query[col] = df_query[col].replace("", "missing")
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
numerical_features,
|
|
650
|
+
boolean_features,
|
|
651
|
+
categorical_features,
|
|
652
|
+
numerical_weights,
|
|
653
|
+
boolean_weights,
|
|
654
|
+
categorical_weights,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def vectorize_records(db_records, query_records, hash_features, weights):
|
|
659
|
+
# Convert database records and query records to DataFrames
|
|
660
|
+
df_db = pd.DataFrame(db_records)
|
|
661
|
+
df_query = pd.DataFrame(query_records)
|
|
662
|
+
|
|
663
|
+
# Determine field types and corresponding weights
|
|
664
|
+
# Modifies boolean columns to True or False
|
|
665
|
+
(
|
|
666
|
+
numerical_features,
|
|
667
|
+
boolean_features,
|
|
668
|
+
categorical_features,
|
|
669
|
+
numerical_weights,
|
|
670
|
+
boolean_weights,
|
|
671
|
+
categorical_weights,
|
|
672
|
+
) = determine_field_types(df_db, df_query, weights)
|
|
673
|
+
|
|
674
|
+
# Fit StandardScaler on the numerical features of the database records
|
|
675
|
+
scaler = StandardScaler()
|
|
676
|
+
if numerical_features:
|
|
677
|
+
df_db[numerical_features] = scaler.fit_transform(df_db[numerical_features])
|
|
678
|
+
df_query[numerical_features] = scaler.transform(df_query[numerical_features])
|
|
679
|
+
|
|
680
|
+
# For db_records
|
|
681
|
+
hashed_categorical_data_db = []
|
|
682
|
+
# For query_records
|
|
683
|
+
hashed_categorical_data_query = []
|
|
684
|
+
|
|
685
|
+
# Process each categorical column separately with its own HashingVectorizer
|
|
686
|
+
for idx, col in enumerate(categorical_features):
|
|
687
|
+
# Create a separate HashingVectorizer for each column to avoid hash collisions
|
|
688
|
+
hashing_vectorizer = HashingVectorizer(
|
|
689
|
+
n_features=hash_features, alternate_sign=False
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Combine all unique values from both db and query for this column to fit the vectorizer
|
|
693
|
+
all_values_for_col = pd.concat([df_db[col], df_query[col]]).unique()
|
|
694
|
+
|
|
695
|
+
# Fit the vectorizer on all unique values to ensure consistency
|
|
696
|
+
hashing_vectorizer.fit(all_values_for_col)
|
|
697
|
+
|
|
698
|
+
# Transform db and query data for this column
|
|
699
|
+
hashed_db = hashing_vectorizer.transform(df_db[col]).toarray()
|
|
700
|
+
hashed_query = hashing_vectorizer.transform(df_query[col]).toarray()
|
|
701
|
+
|
|
702
|
+
# Apply weight to the hashed vectors
|
|
703
|
+
hashed_db_weighted = hashed_db * categorical_weights[idx]
|
|
704
|
+
hashed_query_weighted = hashed_query * categorical_weights[idx]
|
|
705
|
+
|
|
706
|
+
hashed_categorical_data_db.append(hashed_db_weighted)
|
|
707
|
+
hashed_categorical_data_query.append(hashed_query_weighted)
|
|
708
|
+
|
|
709
|
+
# Combine all feature types into a single vector for the database records
|
|
710
|
+
db_vectors = []
|
|
711
|
+
if numerical_features:
|
|
712
|
+
db_vectors.append(df_db[numerical_features].values * numerical_weights)
|
|
713
|
+
if boolean_features:
|
|
714
|
+
db_vectors.append(df_db[boolean_features].astype(int).values * boolean_weights)
|
|
715
|
+
if hashed_categorical_data_db:
|
|
716
|
+
db_vectors.append(np.hstack(hashed_categorical_data_db))
|
|
717
|
+
|
|
718
|
+
# Concatenate database vectors
|
|
719
|
+
final_db_vectors = np.hstack(db_vectors)
|
|
720
|
+
|
|
721
|
+
# Combine all feature types into a single vector for the query records
|
|
722
|
+
query_vectors = []
|
|
723
|
+
if numerical_features:
|
|
724
|
+
query_vectors.append(df_query[numerical_features].values * numerical_weights)
|
|
725
|
+
if boolean_features:
|
|
726
|
+
query_vectors.append(
|
|
727
|
+
df_query[boolean_features].astype(int).values * boolean_weights
|
|
728
|
+
)
|
|
729
|
+
if hashed_categorical_data_query:
|
|
730
|
+
query_vectors.append(np.hstack(hashed_categorical_data_query))
|
|
731
|
+
|
|
732
|
+
# Concatenate query vectors
|
|
733
|
+
final_query_vectors = np.hstack(query_vectors)
|
|
734
|
+
|
|
735
|
+
return final_db_vectors, final_query_vectors
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def split_and_filter_fields(fields: T.List[str]) -> T.Tuple[T.List[str], T.List[str]]:
|
|
739
|
+
# List to store non-lookup fields (load fields)
|
|
740
|
+
load_fields = []
|
|
741
|
+
|
|
742
|
+
# Set to store unique first components of select fields
|
|
743
|
+
unique_components = set()
|
|
744
|
+
# Keep track of last flattened lookup index
|
|
745
|
+
last_flat_lookup_index = -1
|
|
746
|
+
|
|
747
|
+
# Iterate through the fields
|
|
748
|
+
for idx, field in enumerate(fields):
|
|
749
|
+
if "." in field:
|
|
750
|
+
# Split the field by '.' and add the first component to the set
|
|
751
|
+
first_component = field.split(".")[0]
|
|
752
|
+
unique_components.add(first_component)
|
|
753
|
+
last_flat_lookup_index = max(last_flat_lookup_index, idx)
|
|
754
|
+
else:
|
|
755
|
+
# Add the field to the load_fields list
|
|
756
|
+
load_fields.append(field)
|
|
757
|
+
|
|
758
|
+
# Number of unique components
|
|
759
|
+
num_unique_components = len(unique_components)
|
|
760
|
+
|
|
761
|
+
# Adjust select_fields by removing only the field at last_flat_lookup_index + 1
|
|
762
|
+
if last_flat_lookup_index + 1 < len(
|
|
763
|
+
fields
|
|
764
|
+
) and last_flat_lookup_index + num_unique_components < len(fields):
|
|
765
|
+
select_fields = (
|
|
766
|
+
fields[: last_flat_lookup_index + 1]
|
|
767
|
+
+ fields[last_flat_lookup_index + num_unique_components + 1 :]
|
|
768
|
+
)
|
|
769
|
+
else:
|
|
770
|
+
select_fields = fields
|
|
771
|
+
|
|
772
|
+
return load_fields, select_fields
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# Function to reorder records based on the new field list
|
|
776
|
+
def reorder_records(records, original_fields, new_fields):
|
|
777
|
+
if not original_fields:
|
|
778
|
+
raise KeyError("original_fields should not be empty")
|
|
779
|
+
# Map the original field indices
|
|
780
|
+
field_index_map = {field: i for i, field in enumerate(original_fields)}
|
|
781
|
+
reordered_records = []
|
|
782
|
+
|
|
783
|
+
for record in records:
|
|
784
|
+
reordered_records.append(
|
|
785
|
+
[
|
|
786
|
+
record[field_index_map[field]]
|
|
787
|
+
for field in new_fields
|
|
788
|
+
if field in field_index_map
|
|
789
|
+
]
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
return reordered_records
|