From 7fc17f6bcba1be5e41724f7340edfcbb204a89b0 Mon Sep 17 00:00:00 2001 From: David Reed Date: Wed, 24 May 2023 17:33:23 -0600 Subject: [PATCH 01/98] Add deploy transform to inject org URLs (#3596) --- .../tests/test_transforms.py | 38 +++++++++++++++++-- .../core/source_transforms/transforms.py | 15 +++++++- docs/deploy.md | 16 ++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/cumulusci/core/source_transforms/tests/test_transforms.py b/cumulusci/core/source_transforms/tests/test_transforms.py index cf255eb974..4fb9015d5d 100644 --- a/cumulusci/core/source_transforms/tests/test_transforms.py +++ b/cumulusci/core/source_transforms/tests/test_transforms.py @@ -522,9 +522,7 @@ def test_find_replace_current_user(task_context): options = FindReplaceTransformOptions.parse_obj( { "patterns": [ - { - "find": "%%%CURRENT_USER%%%", - }, + {"find": "%%%CURRENT_USER%%%", "inject_username": True}, ] } ) @@ -551,6 +549,40 @@ def test_find_replace_current_user(task_context): ) +def test_find_replace_org_url(task_context): + options = FindReplaceTransformOptions.parse_obj( + { + "patterns": [ + { + "find": "{url}", + "inject_org_url": True, + }, + ] + } + ) + builder = MetadataPackageZipBuilder.from_zipfile( + ZipFileSpec( + { + Path("classes") / "Foo.cls": "System.debug('{url}');", + Path("Bar.cls"): "System.debug('blah');", + } + ).as_zipfile(), + context=task_context, + transforms=[FindReplaceTransform(options)], + ) + + instance_url = task_context.org_config.instance_url + assert ( + ZipFileSpec( + { + Path("classes") / "Foo.cls": f"System.debug('{instance_url}');", + Path("Bar.cls"): "System.debug('blah');", + } + ) + == builder.zf + ) + + @pytest.mark.parametrize("api", [FindReplaceIdAPI.REST, FindReplaceIdAPI.TOOLING]) def test_find_replace_id(api): context = mock.Mock() diff --git a/cumulusci/core/source_transforms/transforms.py b/cumulusci/core/source_transforms/transforms.py index 15e4d04b68..146802bb50 100644 --- a/cumulusci/core/source_transforms/transforms.py +++ b/cumulusci/core/source_transforms/transforms.py @@ -347,7 +347,7 @@ def get_replace_string(self, context: TaskContext) -> str: class FindReplaceCurrentUserSpec(FindReplaceBaseSpec): - inject_username: bool = True + inject_username: bool def get_replace_string(self, context: TaskContext) -> str: if not self.inject_username: # pragma: no cover @@ -358,6 +358,18 @@ def get_replace_string(self, context: TaskContext) -> str: return context.org_config.username +class FindReplaceOrgUrlSpec(FindReplaceBaseSpec): + inject_org_url: bool + + def get_replace_string(self, context: TaskContext) -> str: + if not self.inject_org_url: # pragma: no cover + self.logger.warning( + "The inject_org_url value for the find_replace transform is set to False. Skipping transform." + ) + return self.find + return context.org_config.instance_url + + class FindReplaceTransformOptions(BaseModel): patterns: T.List[ T.Union[ @@ -365,6 +377,7 @@ class FindReplaceTransformOptions(BaseModel): FindReplaceEnvSpec, FindReplaceIdSpec, FindReplaceCurrentUserSpec, + FindReplaceOrgUrlSpec, ] ] diff --git a/docs/deploy.md b/docs/deploy.md index f06e0ac397..df3b50aefb 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -153,6 +153,22 @@ options: inject_username: True ``` +#### Find-and-Replace Org URL Injection + +CumulusCI can replace a given token with the org URL of the target Salesforce org. +All that is needed is to specify a value for `find` and set `inject_org_url: True`: + +```yaml +task: deploy +options: + transforms: + - transform: find_replace + options: + patterns: + - find: special_string + inject_org_url: True +``` + ### Stripping Components with a `package.xml` Manifest This transformation allows you to deploy a subset of a metadata directory based on a `package.xml` manifest by removing unwanted components. It will compare components available in the source folder with a provided `package.xml` file and delete/modify component files which are not found. From 5c68cf992df980548f8ebaecb4925595ecc60b76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 22:49:22 +0000 Subject: [PATCH 02/98] Release v3.76.0 (#3599) --- cumulusci/__about__.py | 2 +- docs/history.md | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 6aa197ac0a..c07daa61d1 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.75.1" +__version__ = "3.76.0" diff --git a/docs/history.md b/docs/history.md index 48066de2e1..4833ef6eaa 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,21 @@ +## v3.76.0 (2023-05-25) + + + +## What's Changed + +### Changes 🎉 + +- Added opt-in Pydantic-based task option parsing. by [@prescod](https://github.com/prescod) in [#1618](https://github.com/SFDO-Tooling/CumulusCI/pull/1618) +- Add deploy transform to inject org URLs by [@davidmreed](https://github.com/davidmreed) in [#3596](https://github.com/SFDO-Tooling/CumulusCI/pull/3596) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.75.1...v3.76.0 + + + ## v3.75.1 (2023-04-14) @@ -14,8 +29,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.75.0...v3.75.1 - - ## v3.75.0 (2023-04-13) From 740832df475cb8b7bfc060dad748cb1bfe5f026d Mon Sep 17 00:00:00 2001 From: David Reed Date: Thu, 25 May 2023 17:21:34 -0600 Subject: [PATCH 03/98] Use Marketing Cloud's validate endpoint to update deployment packages (#3598) --- cumulusci/tasks/marketing_cloud/deploy.py | 238 ++++++++++++++---- .../marketing_cloud/tests/test_deploy.py | 222 +++++++++++++--- .../tests/validation-response.json | 39 +++ 3 files changed, 406 insertions(+), 93 deletions(-) create mode 100644 cumulusci/tasks/marketing_cloud/tests/validation-response.json diff --git a/cumulusci/tasks/marketing_cloud/deploy.py b/cumulusci/tasks/marketing_cloud/deploy.py index df68d3bff3..840fe942b4 100644 --- a/cumulusci/tasks/marketing_cloud/deploy.py +++ b/cumulusci/tasks/marketing_cloud/deploy.py @@ -2,7 +2,9 @@ import uuid import zipfile from collections import defaultdict +from enum import Enum from pathlib import Path +from typing import Dict, Optional import requests @@ -14,7 +16,9 @@ from .base import BaseMarketingCloudTask -MCPM_ENDPOINT = "https://spf.{}.marketingcloudapps.com/api" +MCPM_BASE_ENDPOINT = "https://spf.{}.marketingcloudapps.com/api" + +MCPM_JOB_ID_HEADER = "x-mcpm-job-id" PAYLOAD_CONFIG_VALUES = {"preserveCategories": True} @@ -25,14 +29,25 @@ "timestamp": True, } -IN_PROGRESS_STATUS = "IN_PROGRESS" +IN_PROGRESS_STATUSES = ("NOT_STARTED", "IN_PROGRESS") FINISHED_STATUSES = ("DONE",) ERROR_STATUSES = ("FATAL_ERROR", "ERROR") + UNKNOWN_STATUS_MESSAGE = "Received unknown deploy status: {}" +class PollAction(Enum): + validating = "VALIDATING" + deploying = "DEPLOYING" + + class MarketingCloudDeployTask(BaseMarketingCloudTask): + # This task executes multiple polling loops. + # This enables the task to determine which endpoints should be polled. + current_action: Optional[PollAction] = None + validation_not_found_count = 0 + job_name: Optional[str] task_options = { "package_zip_file": { @@ -62,6 +77,9 @@ def _init_options(self, kwargs): ) def _run_task(self): + # Marketing Cloud validation _requires_ a 2-second static polling interval + self.poll_interval_s = 2 + pkg_zip_file = Path(self.options["package_zip_file"]) if not pkg_zip_file.is_file(): self.logger.error(f"Package zip file not valid: {pkg_zip_file.name}") @@ -72,60 +90,26 @@ def _run_task(self): zf.extractall(temp_dir) payload = self._construct_payload(Path(temp_dir), self.custom_inputs) + # These initialization steps are not done in _init_options() + # because they use MC authorization. We don't want to freeze + # the responses. + endpoint_option = self.options.get("endpoint") + self.endpoint = endpoint_option or MCPM_BASE_ENDPOINT.format( + self.get_mc_stack_key() + ) + self.headers = { "Authorization": f"Bearer {self.mc_config.access_token}", "SFMC-TSSD": self.mc_config.tssd, } - custom_endpoint = self.options.get("endpoint") - self.endpoint = ( - custom_endpoint - if custom_endpoint - else MCPM_ENDPOINT.format(self.get_mc_stack_key()) - ) - self.logger.info(f"Deploying package to: {self.endpoint}/deployments") - response = requests.post( - f"{self.endpoint}/deployments", - json=payload, - headers=self.headers, - ) - response_data = safe_json_from_response(response) + self._validate_package(payload) + self._reset_poll() - self.job_id = response_data["id"] - self.logger.info(f"Started deploy job with Id: {self.job_id}") - self._poll() - - def _poll_action(self): - """ - Poll something and process the response. - Set `self.poll_complete = True` to break polling loop. - """ - response = requests.get( - f"{self.endpoint}/deployments/{self.job_id}", headers=self.headers + payload = self._update_payload_entities_with_actions( + payload, ) - response_data = safe_json_from_response(response) - deploy_status = response_data["status"] - self.logger.info(f"Deployment status is: {deploy_status}") - - if deploy_status != IN_PROGRESS_STATUS: - self._process_completed_deploy(response_data) - - def _process_completed_deploy(self, response_data: dict): - deploy_status = response_data["status"] - assert ( - deploy_status != IN_PROGRESS_STATUS - ), "Deploy should be in a completed state before processing." - - if deploy_status in FINISHED_STATUSES: - self.poll_complete = True - self._validate_response(response_data) - elif deploy_status in ERROR_STATUSES: - self.poll_complete = True - self._report_error(response_data) - else: - self.logger.error(UNKNOWN_STATUS_MESSAGE.format(deploy_status)) - self.poll_complete = True - self._report_error(response_data) + self._deploy_package(payload) def _construct_payload(self, dir_path, custom_inputs=None): dir_path = Path(dir_path) @@ -134,7 +118,7 @@ def _construct_payload(self, dir_path, custom_inputs=None): payload = defaultdict(lambda: defaultdict(dict)) payload["namespace"] = PAYLOAD_NAMESPACE_VALUES payload["config"] = PAYLOAD_CONFIG_VALUES - payload["name"] = self.options.get("name", str(uuid.uuid4())) + payload["name"] = self.job_name = self.options.get("name", str(uuid.uuid4())) # type: ignore try: with open(dir_path / "info.json", "r") as f: @@ -187,9 +171,153 @@ def _add_custom_inputs_to_payload(self, custom_inputs, payload): return payload - def _validate_response(self, deploy_info: dict): + def _validate_package(self, payload: Dict) -> None: + """Sends the payload to MC for validation. + Returns a dict of allowable actions for the target MC instance.""" + self.current_action = PollAction.validating + self.logger.info(f"Validating package at: {self.endpoint}/validate") + assert self.job_name + response = requests.post( + f"{self.endpoint}/validate", + json=payload, + headers={ + MCPM_JOB_ID_HEADER: self.job_name, + **self.headers, + }, + ) + response.raise_for_status() + response_data = safe_json_from_response(response) + self.validate_id = response_data["id"] + self.logger.info(f"Started package validation with Id: {self.validate_id}") + self._poll() + + def _update_payload_entities_with_actions(self, payload: Dict) -> Dict: + """Include available entity action returned from the validation + endpoint into the payload used for package deployment.""" + + for entity_type in payload["entities"]: + this_entity_type = payload["entities"][entity_type] + for entity_id in this_entity_type: + this_entity = payload["entities"][entity_type][entity_id] + action = self.action_for_entity(entity_type, entity_id) + this_entity["action"] = action + + if self.debug_mode: + self.logger.debug(f"Payload updated with actions:\n{json.dumps(payload)}") + + return payload + + def action_for_entity(self, entity: str, entity_id: str) -> Optional[Dict]: + """Fetch the corresponding action for the given entity with the specified Id""" + try: + for action_info in self.validation_response["entities"][entity][entity_id][ + "actions" + ].values(): + if action_info["available"]: + return action_info + except KeyError: + # if no actions are defined for this entity just move on + pass + + def _deploy_package(self, payload: Dict) -> None: + self.current_action = PollAction.deploying + self.logger.info(f"Deploying package to: {self.endpoint}/deployments") + response = requests.post( + f"{self.endpoint}/deployments", + json=payload, + headers=self.headers, + ) + response.raise_for_status() + response_data = safe_json_from_response(response) + self.deploy_id = response_data["id"] + self.logger.info(f"Started package deploy with Id: {self.validate_id}") + self._poll() + + def _poll_action(self) -> None: + """ + Poll based on the current action being taken. + """ + if self.current_action == PollAction.validating: + self._poll_validating() + elif self.current_action == PollAction.deploying: + self._poll_deploying() + else: + raise Exception( + f"PollAction {self.current_action} does not have a polling handler defined." + ) + + def _poll_validating(self) -> None: + assert self.job_name + response = requests.get( + f"{self.endpoint}/validate/{self.validate_id}", + headers={MCPM_JOB_ID_HEADER: self.job_name, **self.headers}, + ) + response_data = safe_json_from_response(response) + validation_status = response_data["status"] + self.logger.info(f"Validation status is: {validation_status}") + + # Handle eccentricities of Marketing Cloud validation polling. + # We may get back a NOT_FOUND result if our request is routed + # to the wrong pod by MC. This should be fixed, but hasn't been + # deployed to all MC stacks yet. + + # Observed behavior is that response switches between NOT_FOUND + # and IN_PROGRESS. We'll track these responses and fail the job + # only if we consistently receive NOT_FOUND. + + # TODO: add a timeout. + if validation_status == "NOT_FOUND": + self.validation_not_found_count += 1 + if self.validation_not_found_count > 10: + raise DeploymentException( + f"Unable to find status on validation: {self.validate_id}" + ) + else: + # Reset if we get back a result other than NOT_FOUND + self.validation_not_found_count = 0 + if validation_status not in IN_PROGRESS_STATUSES: + self.poll_complete = True + if self.debug_mode: # pragma: nocover + self.logger.debug( + f"Validation Response:\n{json.dumps(response_data)}" + ) + self.validation_response = response_data + + def _poll_deploying(self) -> None: + response = requests.get( + f"{self.endpoint}/deployments/{self.deploy_id}", headers=self.headers + ) + response_data = safe_json_from_response(response) + deploy_status = response_data["status"] + self.logger.info(f"Deployment status is: {deploy_status}") + + if deploy_status not in IN_PROGRESS_STATUSES: + self._process_completed_deploy(response_data) + + def _poll_update_interval(self): + # Marketing Cloud validation _requires_ a 2-second static polling interval. + # Override the base class to remove backoff. + pass + + def _process_completed_deploy(self, response_data: Dict): + deploy_status = response_data["status"] + assert ( + deploy_status != IN_PROGRESS_STATUSES + ), "Deploy should be in a completed state before processing." + + self.poll_complete = True + if deploy_status in FINISHED_STATUSES: + self._validate_response(response_data) + elif deploy_status in ERROR_STATUSES: + self._report_error(response_data) + else: + self.logger.error(UNKNOWN_STATUS_MESSAGE.format(deploy_status)) + self._report_error(response_data) + + def _validate_response(self, deploy_info: Dict) -> None: """Checks for any errors present in the response to the deploy request. - Displays errors if present, else informs use that the deployment was successful.""" + Displays errors if present, else informs use that the deployment was successful. + """ has_error = False for entity, info in deploy_info["entities"].items(): if not info: @@ -205,12 +333,12 @@ def _validate_response(self, deploy_info: dict): if has_error: raise DeploymentException("Marketing Cloud reported deployment failures.") - self.logger.info("Deployment completed successfully.") + self.logger.info(f"Deployment ({self.deploy_id}) completed successfully.") - def _report_error(self, response_data: dict): + def _report_error(self, response_data: Dict) -> None: deploy_status = response_data["status"] self.logger.error( - f"Received status of: {deploy_status}. Received the following data from Marketing Cloud:\n{response_data}^" + f"Received status of: {deploy_status}. Received the following data from Marketing Cloud:\n{json.dumps(response_data)}" ) raise DeploymentException( f"Marketing Cloud deploy finished with status of: {deploy_status}" diff --git a/cumulusci/tasks/marketing_cloud/tests/test_deploy.py b/cumulusci/tasks/marketing_cloud/tests/test_deploy.py index c018b99e14..1a915dbe8e 100644 --- a/cumulusci/tasks/marketing_cloud/tests/test_deploy.py +++ b/cumulusci/tasks/marketing_cloud/tests/test_deploy.py @@ -13,7 +13,8 @@ ) from cumulusci.core.exceptions import DeploymentException from cumulusci.tasks.marketing_cloud.deploy import ( - MCPM_ENDPOINT, + MCPM_BASE_ENDPOINT, + MCPM_JOB_ID_HEADER, UNKNOWN_STATUS_MESSAGE, MarketingCloudDeployTask, ) @@ -24,6 +25,12 @@ STACK_KEY = "S4" TEST_ZIPFILE = "test-mc-pkg.zip" PAYLOAD_FILE = "expected-payload.json" +JOB_NAME = "foo_job" + +# This JSON does not necessarily include the complete data provided by Marketing Cloud +# It is intended to exercise functionality in the class under test. +# The response is designed to go with the content of `expected-payload.json` +VALIDATION_RESPONSE_FILE = "validation-response.json" @pytest.fixture @@ -36,6 +43,7 @@ def task(project_config): "options": { "package_zip_file": test_zip_file.resolve(), "custom_inputs": "companyName:Acme", + "name": JOB_NAME, } } ), @@ -48,9 +56,23 @@ def task(project_config): "mc", None, ) + task.poll_interval_s = 0 # Do not poll during testing return task +@pytest.fixture +def validation_response(): + with open(Path(__file__).parent.absolute() / VALIDATION_RESPONSE_FILE) as f: + return json.load(f) + + +@pytest.fixture +def expected_payload(): + expected_payload_file = Path(__file__).parent.absolute() / PAYLOAD_FILE + with open(expected_payload_file, "r") as f: + return json.load(f) + + @pytest.fixture def task_without_custom_inputs(project_config): test_zip_file = Path(__file__).parent.absolute() / TEST_ZIPFILE @@ -61,6 +83,7 @@ def task_without_custom_inputs(project_config): "options": { "package_zip_file": test_zip_file.resolve(), "custom_inputs": None, + "name": JOB_NAME, } } ), @@ -87,54 +110,133 @@ def mocked_responses(): yield rsps -class TestMarketingCloudDeployTask: - def test_run_task__deploy_succeeds_with_custom_inputs(self, task, mocked_responses): +@pytest.fixture +def mocked_validation_responses(mocked_responses): + mocked_responses.add( + "POST", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate", + json={"id": "0be865fe-efb2-479d-99c2-c1b608155369"}, + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + ) + mocked_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate/0be865fe-efb2-479d-99c2-c1b608155369", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"status": "DONE"}, + ) + + yield mocked_responses + + +@pytest.fixture +def mocked_polling_validation_responses(mocked_responses, validation_response): + mocked_responses.add( + "POST", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"id": "0be865fe-efb2-479d-99c2-c1b608155369"}, + ) + mocked_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate/0be865fe-efb2-479d-99c2-c1b608155369", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"status": "NOT_FOUND"}, + ) + mocked_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate/0be865fe-efb2-479d-99c2-c1b608155369", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"status": "IN_PROGRESS"}, + ) + mocked_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate/0be865fe-efb2-479d-99c2-c1b608155369", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"status": "NOT_FOUND"}, + ) + mocked_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate/0be865fe-efb2-479d-99c2-c1b608155369", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json=validation_response, + ) + + yield mocked_responses + + +@pytest.fixture +def mocked_polling_validation_responses_all_not_found(mocked_responses): + mocked_responses.add( + "POST", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"id": "0be865fe-efb2-479d-99c2-c1b608155369"}, + ) + for _ in range(10): mocked_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/validate/0be865fe-efb2-479d-99c2-c1b608155369", + match=[responses.matchers.header_matcher({MCPM_JOB_ID_HEADER: JOB_NAME})], + json={"status": "NOT_FOUND"}, + ) + + yield mocked_responses + + +class TestMarketingCloudDeployTask: + def test_run_task__deploy_succeeds_with_custom_inputs( + self, task, mocked_validation_responses + ): + mocked_validation_responses.add( "POST", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments", json={"id": "JOBID", "status": "IN_PROGRESS"}, ) - mocked_responses.add( + mocked_validation_responses.add( "GET", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", json={"status": "DONE", "entities": {}}, ) task.logger = mock.Mock() task._run_task() - task.logger.info.assert_called_with("Deployment completed successfully.") + task.logger.info.assert_called_with( + "Deployment (JOBID) completed successfully." + ) assert task.logger.error.call_count == 0 assert task.logger.warn.call_count == 0 def test_run_task__deploy_succeeds_without_custom_inputs( - self, task_without_custom_inputs, mocked_responses + self, task_without_custom_inputs, mocked_validation_responses ): - mocked_responses.add( + mocked_validation_responses.add( "POST", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments", json={"id": "JOBID", "status": "IN_PROGRESS"}, ) - mocked_responses.add( + mocked_validation_responses.add( "GET", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", json={"status": "DONE", "entities": {}}, ) task = task_without_custom_inputs task.logger = mock.Mock() task._run_task() - task.logger.info.assert_called_with("Deployment completed successfully.") + task.logger.info.assert_called_with( + "Deployment (JOBID) completed successfully." + ) assert task.logger.error.call_count == 0 assert task.logger.warn.call_count == 0 - def test_run_task__deploy_fails(self, task, mocked_responses): - mocked_responses.add( + def test_run_task__deploy_fails(self, task, mocked_validation_responses): + mocked_validation_responses.add( "POST", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments", json={"id": "JOBID", "status": "IN_PROGRESS"}, ) - mocked_responses.add( + mocked_validation_responses.add( "GET", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", json={ "status": "DONE", "entities": { @@ -160,15 +262,15 @@ def test_run_task__deploy_fails(self, task, mocked_responses): == "Failed to deploy assets/1. Status: SKIPPED. Issues: ['A problem occurred']" ) - def test_run_task__FATAL_ERROR_result(self, task, mocked_responses): - mocked_responses.add( + def test_run_task__FATAL_ERROR_result(self, task, mocked_validation_responses): + mocked_validation_responses.add( "POST", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments", json={"id": "JOBID", "status": "FATAL_ERROR"}, ) - mocked_responses.add( + mocked_validation_responses.add( "GET", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", json={ "status": "FATAL_ERROR", "entities": {}, @@ -178,16 +280,18 @@ def test_run_task__FATAL_ERROR_result(self, task, mocked_responses): with pytest.raises(DeploymentException): task._run_task() - def test_run_task__unknown_deploy_status(self, task, mocked_responses, caplog): + def test_run_task__unknown_deploy_status( + self, task, mocked_validation_responses, caplog + ): unknown_status = "FOOBAR" - mocked_responses.add( + mocked_validation_responses.add( "POST", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments", json={"id": "JOBID"}, ) - mocked_responses.add( + mocked_validation_responses.add( "GET", - f"{MCPM_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", json={ "status": unknown_status, "entities": {}, @@ -200,6 +304,54 @@ def test_run_task__unknown_deploy_status(self, task, mocked_responses, caplog): logged_messages = [log.message for log in caplog.records] assert UNKNOWN_STATUS_MESSAGE.format(unknown_status) in logged_messages + # NOTE: the x-mcpm-job-id header is validated implicitly by all use + # of Responses mocks. + + def test_validate_package__validation_accepts_intermittent_not_found( + self, task, mocked_polling_validation_responses + ): + mocked_polling_validation_responses.add( + "POST", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments", + json={"id": "JOBID", "status": "IN_PROGRESS"}, + ) + mocked_polling_validation_responses.add( + "GET", + f"{MCPM_BASE_ENDPOINT.format(STACK_KEY)}/deployments/JOBID", + json={"status": "DONE", "entities": {}}, + ) + task._run_task() + assert task.validation_response["status"] == "DONE" + + def test_validate_package__validation_fails_consistent_not_found( + self, task, mocked_polling_validation_responses_all_not_found + ): + with pytest.raises(DeploymentException): + task._run_task() + + def test_update_payload_entities_with_actions( + self, task, expected_payload, validation_response + ): + task.validation_response = validation_response + validated_payload = task._update_payload_entities_with_actions(expected_payload) + + assert validated_payload["entities"]["assets"]["0"]["action"] == { + "type": "create", + "available": True, + "issues": [], + } + + def test_action_for_entity(self, task, validation_response): + task.validation_response = validation_response + + # We should expect to get back the first action with available == True + assert task.action_for_entity("assets", "0") == { + "type": "create", + "available": True, + "issues": [], + } + assert task.action_for_entity("foo", "0") is None + def test_zipfile_not_valid(self, task): task.options["package_zip_file"] = "not-a-valid-file.zip" task.logger = mock.Mock() @@ -209,25 +361,19 @@ def test_zipfile_not_valid(self, task): "Package zip file not valid: not-a-valid-file.zip" ) - @mock.patch("cumulusci.tasks.marketing_cloud.deploy.uuid") - def test_construct_payload(self, uuid, task): - uuid.uuid4.return_value = "cci-deploy" + def test_construct_payload(self, task, expected_payload): + task.options["name"] = "cci-deploy" pkg_zip_file = Path(task.options["package_zip_file"]) with temporary_dir() as temp_dir: with zipfile.ZipFile(pkg_zip_file) as zf: zf.extractall(temp_dir) actual_payload = task._construct_payload(Path(temp_dir)) - expected_payload_file = Path(__file__).parent.absolute() / PAYLOAD_FILE - with open(expected_payload_file, "r") as f: - expected_payload = json.load(f) - assert expected_payload == actual_payload - @mock.patch("cumulusci.tasks.marketing_cloud.deploy.uuid") - def test_construct_payload__file_not_found(self, uuid, task): + def test_construct_payload__file_not_found(self, task): """Ensure we state clearly state where expect files to be""" - uuid.uuid4.return_value = "cci-deploy" + task.options["name"] = "cci-deploy" pkg_zip_file = Path(task.options["package_zip_file"]) with temporary_dir() as temp_dir: with zipfile.ZipFile(pkg_zip_file) as zf: diff --git a/cumulusci/tasks/marketing_cloud/tests/validation-response.json b/cumulusci/tasks/marketing_cloud/tests/validation-response.json new file mode 100644 index 0000000000..ee14613b49 --- /dev/null +++ b/cumulusci/tasks/marketing_cloud/tests/validation-response.json @@ -0,0 +1,39 @@ +{ + "status": "DONE", + "entities": { + "assets": { + "0": { + "issues": [], + "data": {}, + "action": { + "type": "create", + "available": true, + "issues": [] + }, + "actions": { + "update": { + "type": "update", + "available": false, + "issues": [] + }, + "create": { + "type": "create", + "available": true, + "issues": [] + }, + "reuse": { + "type": "reuse", + "available": false, + "issues": [] + }, + "skip": { + "type": "skip", + "available": false + } + }, + "renameable": true, + "status": "READY" + } + } + } +} From c61b823e7d64c27e57206ae78612db0c9eacddcb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 09:00:31 -0600 Subject: [PATCH 04/98] Release 3.76.0.dev0 Co-authored-by: github-actions --- cumulusci/__about__.py | 2 +- docs/history.md | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index c07daa61d1..3eeebaab27 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.76.0" +__version__ = "3.76.0.dev0" diff --git a/docs/history.md b/docs/history.md index 4833ef6eaa..2f7d1e4e95 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,20 @@ +## v3.76.0.dev0 (2023-05-25) + + + +## What's Changed + +### Changes 🎉 + +- Use Marketing Cloud's validate endpoint to update deployment packages by [@davidmreed](https://github.com/davidmreed) and [@TheBitShepherd](https://github.com/TheBitShepherd) in [#3598](https://github.com/SFDO-Tooling/CumulusCI/pull/3598) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.76.0...v3.76.0.dev0 + + + ## v3.76.0 (2023-05-25) @@ -15,8 +29,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.75.1...v3.76.0 - - ## v3.75.1 (2023-04-14) From d4c3155716c68e30762d19822b379c0504407521 Mon Sep 17 00:00:00 2001 From: David Reed Date: Fri, 7 Jul 2023 10:53:16 -0600 Subject: [PATCH 05/98] Add preflight check for PSL assignments (#3616) --- cumulusci/cumulusci.yml | 6 +++- cumulusci/tasks/preflight/licenses.py | 11 +++++++ .../tasks/preflight/tests/test_licenses.py | 32 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 7ab9af5349..57cd1e0220 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -311,9 +311,13 @@ tasks: class_path: cumulusci.tasks.preflight.licenses.GetAvailablePermissionSetLicenses group: Salesforce Preflight Checks get_assigned_permission_sets: - description: Retrieves a list of the names of any permission sets assigned to the running user. + description: Retrieves a list of the developer names of any permission sets assigned to the running user. class_path: cumulusci.tasks.preflight.permsets.GetPermissionSetAssignments group: Salesforce Preflight Checks + get_assigned_permission_set_licenses: + description: Retrieves a list of the developer names of any Permission Set Licenses assigned to the running user. + class_path: cumulusci.tasks.preflight.licenses.GetPermissionLicenseSetAssignments + group: Salesforce Preflight Checks get_available_permission_sets: description: Retrieves a list of the currently available Permission Sets class_path: cumulusci.tasks.preflight.licenses.GetAvailablePermissionSets diff --git a/cumulusci/tasks/preflight/licenses.py b/cumulusci/tasks/preflight/licenses.py index 3bf6853dc2..cc40ed1591 100644 --- a/cumulusci/tasks/preflight/licenses.py +++ b/cumulusci/tasks/preflight/licenses.py @@ -25,6 +25,17 @@ def _run_task(self): self.logger.info(f"Found permission set licenses:\n{licenses}") +class GetPermissionLicenseSetAssignments(BaseSalesforceApiTask): + def _run_task(self): + query = f"SELECT PermissionSetLicense.DeveloperName FROM PermissionSetLicenseAssign WHERE AssigneeId = '{self.org_config.user_id}'" + self.return_values = [ + result["PermissionSetLicense"]["DeveloperName"] + for result in self.sf.query_all(query)["records"] + ] + permsets = "\n".join(self.return_values) + self.logger.info(f"Found permission licenses sets assigned:\n{permsets}") + + class GetAvailablePermissionSets(BaseSalesforceApiTask): def _run_task(self): self.return_values = [ diff --git a/cumulusci/tasks/preflight/tests/test_licenses.py b/cumulusci/tasks/preflight/tests/test_licenses.py index 413cac9f87..f2cf117ad6 100644 --- a/cumulusci/tasks/preflight/tests/test_licenses.py +++ b/cumulusci/tasks/preflight/tests/test_licenses.py @@ -4,6 +4,7 @@ GetAvailableLicenses, GetAvailablePermissionSetLicenses, GetAvailablePermissionSets, + GetPermissionLicenseSetAssignments, ) from cumulusci.tasks.salesforce.tests.util import create_task @@ -44,6 +45,37 @@ def test_psl_preflight(self): ) assert task.return_values == ["TEST1", "TEST2"] + def test_assigned_permsetlicense_preflight(self): + task = create_task(GetPermissionLicenseSetAssignments, {}) + task._init_api = Mock() + task._init_api.return_value.query_all.return_value = { + "totalSize": 2, + "done": True, + "records": [ + { + "PermissionSetLicense": { + "MasterLabel": "Document Checklist", + "DeveloperName": "DocumentChecklist", + }, + }, + { + "PermissionSetLicense": { + "MasterLabel": "Einstein Analytics Plus Admin", + "DeveloperName": "EinsteinAnalyticsPlusAdmin", + }, + }, + ], + } + task() + + task._init_api.return_value.query_all.assert_called_once_with( + "SELECT PermissionSetLicense.DeveloperName FROM PermissionSetLicenseAssign WHERE AssigneeId = 'USER_ID'" + ) + assert task.return_values == [ + "DocumentChecklist", + "EinsteinAnalyticsPlusAdmin", + ] + def test_permsets_preflight(self): task = create_task(GetAvailablePermissionSets, {}) task._init_api = Mock() From e5e9d8b2341b644d0b1e1284513b613c8445d3ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Jul 2023 13:21:35 -0600 Subject: [PATCH 06/98] Release v3.77.0 (#3617) --- cumulusci/__about__.py | 2 +- docs/history.md | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 3eeebaab27..418e04e00b 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.76.0.dev0" +__version__ = "3.77.0" diff --git a/docs/history.md b/docs/history.md index 2f7d1e4e95..2fe6a30c78 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,7 +2,7 @@ -## v3.76.0.dev0 (2023-05-25) +## v3.77.0 (2023-07-07) @@ -10,12 +10,25 @@ ### Changes 🎉 +- Add preflight check for PSL assignments by [@davidmreed](https://github.com/davidmreed) in [#3616](https://github.com/SFDO-Tooling/CumulusCI/pull/3616) - Use Marketing Cloud's validate endpoint to update deployment packages by [@davidmreed](https://github.com/davidmreed) and [@TheBitShepherd](https://github.com/TheBitShepherd) in [#3598](https://github.com/SFDO-Tooling/CumulusCI/pull/3598) -**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.76.0...v3.76.0.dev0 +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.76.0...v3.77.0 +## v3.76.0.dev0 (2023-05-25) + + + +## What's Changed + +### Changes 🎉 + +- Use Marketing Cloud's validate endpoint to update deployment packages by [@davidmreed](https://github.com/davidmreed) and [@TheBitShepherd](https://github.com/TheBitShepherd) in [#3598](https://github.com/SFDO-Tooling/CumulusCI/pull/3598) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.76.0...v3.76.0.dev0 + ## v3.76.0 (2023-05-25) From 89d36c9677b8deceda8b8dd87e1dbd8725b517bb Mon Sep 17 00:00:00 2001 From: James Estevez Date: Tue, 18 Jul 2023 10:02:23 -0700 Subject: [PATCH 07/98] Fix skip_future_releases option (#3624) Fixes #3594 As written, skip_future_releases is always `True` for `skip_future_releases: False` or `skip_future_releases: false` because those get converted to booleans before being passed to `process_bool_arg`. This was working as designed when run through the CLI because the option doesn't go through the Pydantic model so it's a `str`. --- cumulusci/tasks/github/merge.py | 9 +- cumulusci/tasks/github/tests/test_merge.py | 106 ++++++++++++++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/cumulusci/tasks/github/merge.py b/cumulusci/tasks/github/merge.py index 1134de0042..9d9b7bfbd8 100644 --- a/cumulusci/tasks/github/merge.py +++ b/cumulusci/tasks/github/merge.py @@ -49,9 +49,12 @@ def _init_options(self, kwargs): self.options[ "source_branch" ] = self.project_config.project__git__default_branch - self.options["skip_future_releases"] = process_bool_arg( - self.options.get("skip_future_releases") or True - ) + if "skip_future_releases" not in self.options: + self.options["skip_future_releases"] = True + else: + self.options["skip_future_releases"] = process_bool_arg( + self.options.get("skip_future_releases") + ) self.options["update_future_releases"] = process_bool_arg( self.options.get("update_future_releases") or False ) diff --git a/cumulusci/tasks/github/tests/test_merge.py b/cumulusci/tasks/github/tests/test_merge.py index def5449354..f84b62f296 100644 --- a/cumulusci/tasks/github/tests/test_merge.py +++ b/cumulusci/tasks/github/tests/test_merge.py @@ -641,7 +641,13 @@ def test_branches_to_merge__feature_merge_no_children(self): def test_merge_to_future_release_branches(self): """Tests that commits to the main branch are merged to the expected feature branches""" self._setup_mocks( - ["main", "feature/230", "feature/232", "feature/300", "feature/work-item"] + [ + "main", + "feature/230", + "feature/232", + "feature/300", + "feature/work-item", + ] ) task = self._create_task( @@ -660,6 +666,104 @@ def test_merge_to_future_release_branches(self): assert ["feature/232", "feature/300"] == actual_branches assert 2 == len(responses.calls) + @responses.activate + def test_merge_to_future_release_branches_skip(self): + """Tests that commits to the main branch are merged to the expected feature branches""" + self._setup_mocks( + [ + "main", + "feature/230", + "feature/232", + "feature/300", + "feature/302", + "feature/work-item", + ] + ) + + task = self._create_task( + task_config={ + "options": { + "source_branch": "feature/230", + "branch_prefix": "feature/", + "update_future_releases": False, + } + } + ) + task._init_task() + + actual_branches = [branch.name for branch in task._get_branches_to_merge()] + + assert [] == actual_branches + assert 2 == len(responses.calls) + + @responses.activate + def test_merge_to_future_release_branches_skip_future(self): + """ + Tests that commits are not merged to "release" (numeric) branches + """ + + self._setup_mocks( + [ + "main", + "feature/230", + "feature/232", + "feature/300", + "feature/work-item", + ] + ) + + task = self._create_task( + task_config={ + "options": { + "source_branch": "main", + "branch_prefix": "feature/", + "update_future_releases": False, + "skip_future_releases": True, + } + } + ) + task._init_task() + + actual_branches = [branch.name for branch in task._get_branches_to_merge()] + + assert ["feature/230", "feature/work-item"] == actual_branches + assert 2 == len(responses.calls) + + @responses.activate + def test_merge_to_future_release_branches_noskip_future(self): + """Tests that commits to the main branch are merged to the expected feature branches""" + self._setup_mocks( + [ + "main", + "feature/230", + "feature/232", + "feature/300", + "feature/work-item", + ] + ) + + task = self._create_task( + task_config={ + "options": { + "source_branch": "main", + "branch_prefix": "feature/", + "update_future_releases": False, + "skip_future_releases": False, + } + } + ) + task._init_task() + + actual_branches = [branch.name for branch in task._get_branches_to_merge()] + + assert [ + "feature/230", + "feature/232", + "feature/300", + "feature/work-item", + ] == actual_branches + assert 2 == len(responses.calls) + @responses.activate def test_merge_to_future_release_branches_missing_slash(self): """Tests that commits to the main branch are merged to the expected feature branches""" From c9215c94a02f0cdc057e6cdb65c6cfdd931cbff0 Mon Sep 17 00:00:00 2001 From: Jaipal Reddy Kasturi Date: Tue, 1 Aug 2023 02:42:06 +0530 Subject: [PATCH 08/98] Querying PermissionSetGroups to get list of assigned permission sets (#3623) --- .../core/config/oauth2_service_config.py | 4 +- cumulusci/tasks/preflight/permsets.py | 22 +++++--- .../tests/test_permset_preflights.py | 55 ++++++++++++------- 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/cumulusci/core/config/oauth2_service_config.py b/cumulusci/core/config/oauth2_service_config.py index 98646a8e52..774bd299d2 100644 --- a/cumulusci/core/config/oauth2_service_config.py +++ b/cumulusci/core/config/oauth2_service_config.py @@ -8,8 +8,8 @@ class OAuth2ServiceConfig(ServiceConfig, ABC): """Base class for services that require an OAuth2 Client for establishing a connection.""" - @abstractclassmethod - def connect(self) -> Dict: + @abstractclassmethod # type: ignore + def connect(cls) -> Dict: # type: ignore """This method is called when the service is first connected via `cci service connect`. This method should perform the necessary OAuth flow and return a dict of values that the service would like diff --git a/cumulusci/tasks/preflight/permsets.py b/cumulusci/tasks/preflight/permsets.py index bcf4f9140e..c02ff93f9c 100644 --- a/cumulusci/tasks/preflight/permsets.py +++ b/cumulusci/tasks/preflight/permsets.py @@ -3,10 +3,18 @@ class GetPermissionSetAssignments(BaseSalesforceApiTask): def _run_task(self): - query = f"SELECT PermissionSet.Name FROM PermissionSetAssignment WHERE AssigneeId = '{self.org_config.user_id}'" - self.return_values = [ - result["PermissionSet"]["Name"] - for result in self.sf.query_all(query)["records"] - ] - permsets = "\n".join(self.return_values) - self.logger.info(f"Found permission sets assigned:\n{permsets}") + query = f"SELECT PermissionSet.Name,PermissionSetGroupId FROM PermissionSetAssignment WHERE AssigneeId = '{self.org_config.user_id}'" + + self.permsets = [] + for result in self.sf.query_all(query)["records"]: + if result["PermissionSet"]["Name"] not in self.permsets: + self.permsets.append(result["PermissionSet"]["Name"]) + + if result["PermissionSetGroupId"] is not None: + psg_query = f"SELECT PermissionSet.Name from PermissionSetGroupComponent where PermissionSetGroupId = '{result['PermissionSetGroupId']}'" + for psg_result in self.sf.query_all(psg_query)["records"]: + if psg_result["PermissionSet"]["Name"] not in self.permsets: + self.permsets.append(psg_result["PermissionSet"]["Name"]) + + permsets_str = "\n".join(self.permsets) + self.logger.info(f"Found permission sets assigned:\n{permsets_str}") diff --git a/cumulusci/tasks/preflight/tests/test_permset_preflights.py b/cumulusci/tasks/preflight/tests/test_permset_preflights.py index 17034db492..eded8f95f2 100644 --- a/cumulusci/tasks/preflight/tests/test_permset_preflights.py +++ b/cumulusci/tasks/preflight/tests/test_permset_preflights.py @@ -8,30 +8,45 @@ class TestPermsetPreflights: def test_assigned_permset_preflight(self): task = create_task(GetPermissionSetAssignments, {}) task._init_api = Mock() - task._init_api.return_value.query_all.return_value = { - "totalSize": 2, - "done": True, - "records": [ - { - "PermissionSet": { - "Label": "Document Checklist", - "Name": "DocumentChecklist", + task._init_api.return_value.query_all.side_effect = [ + { + "totalSize": 2, + "done": True, + "records": [ + { + "PermissionSet": { + "Label": "Document Checklist", + "Name": "DocumentChecklist", + }, + "PermissionSetGroupId": None, }, - }, - { - "PermissionSet": { - "Label": "Einstein Analytics Plus Admin", - "Name": "EinsteinAnalyticsPlusAdmin", + { + "PermissionSet": { + "Label": "Einstein Analytics Plus Admin", + "Name": "EinsteinAnalyticsPlusAdmin", + }, + "PermissionSetGroupId": "0PG000000000001", }, - }, - ], - } + ], + }, + { + "totalSize": 1, + "done": True, + "records": [ + { + "PermissionSet": { + "Label": "Customer Experience Analytics Admin", + "Name": "CustomerExperienceAnalyticsAdmin", + }, + }, + ], + }, + ] task() - task._init_api.return_value.query_all.assert_called_once_with( - "SELECT PermissionSet.Name FROM PermissionSetAssignment WHERE AssigneeId = 'USER_ID'" - ) - assert task.return_values == [ + task._init_api.return_value.query_all.assert_called() + assert task.permsets == [ "DocumentChecklist", "EinsteinAnalyticsPlusAdmin", + "CustomerExperienceAnalyticsAdmin", ] From a90d88c525a46d708dcb9de6c4a62c70c85b72e9 Mon Sep 17 00:00:00 2001 From: David Reed Date: Mon, 31 Jul 2023 15:35:09 -0600 Subject: [PATCH 09/98] Add create_pull_request_on_conflict option to automerge tasks (#3632) Co-authored-by: Ben French --- AUTHORS.rst | 1 + cumulusci/tasks/github/merge.py | 60 +++++++++----- cumulusci/tasks/github/tests/test_merge.py | 92 ++++++++++++++++++++++ 3 files changed, 132 insertions(+), 21 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1ac2d6bda3..ba6dd22338 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -36,3 +36,4 @@ For example: * Ed Rivas (jerivas) * Gustavo Tandeciarz (dcinzona) +* Ben French (BenjaminFrench) diff --git a/cumulusci/tasks/github/merge.py b/cumulusci/tasks/github/merge.py index 9d9b7bfbd8..1cc32887fa 100644 --- a/cumulusci/tasks/github/merge.py +++ b/cumulusci/tasks/github/merge.py @@ -34,6 +34,9 @@ class MergeBranch(BaseGithubTask): "update_future_releases": { "description": "If true, then include release branches that are not the lowest release number even if they are not child branches. Defaults to False." }, + "create_pull_request_on_conflict": { + "description": "If true, then create a pull request when a merge conflict arises. Defaults to True." + }, } def _init_options(self, kwargs): @@ -58,6 +61,12 @@ def _init_options(self, kwargs): self.options["update_future_releases"] = process_bool_arg( self.options.get("update_future_releases") or False ) + if "create_pull_request_on_conflict" not in self.options: + self.options["create_pull_request_on_conflict"] = True + else: + self.options["create_pull_request_on_conflict"] = process_bool_arg( + self.options.get("create_pull_request_on_conflict") + ) def _init_task(self): super()._init_task() @@ -245,30 +254,39 @@ def _merge(self, branch_name, source, commit): if e.code != http.client.CONFLICT: raise - if branch_name in self._get_existing_prs( - self.options["source_branch"], self.options["branch_prefix"] - ): - self.logger.info( - f"Merge conflict on branch {branch_name}: merge PR already exists" - ) - return - - try: - pull = self.repo.create_pull( - title=f"Merge {source} into {branch_name}", - base=branch_name, - head=source, - body="This pull request was automatically generated because " - "an automated merge hit a merge conflict", - ) + if self.options["create_pull_request_on_conflict"]: + self._create_conflict_pull_request(branch_name, source) + else: self.logger.info( - f"Merge conflict on branch {branch_name}: created pull request #{pull.number}" - ) - except github3.exceptions.UnprocessableEntity as e: - self.logger.error( - f"Error creating merge conflict pull request to merge {source} into {branch_name}:\n{e.response.text}" + f"Merge conflict on branch {branch_name}: skipping pull request creation" ) + def _create_conflict_pull_request(self, branch_name, source): + """Attempt to create a pull request from source into branch_name if merge operation encounters a conflict""" + if branch_name in self._get_existing_prs( + self.options["source_branch"], self.options["branch_prefix"] + ): + self.logger.info( + f"Merge conflict on branch {branch_name}: merge PR already exists" + ) + return + + try: + pull = self.repo.create_pull( + title=f"Merge {source} into {branch_name}", + base=branch_name, + head=source, + body="This pull request was automatically generated because " + "an automated merge hit a merge conflict", + ) + self.logger.info( + f"Merge conflict on branch {branch_name}: created pull request #{pull.number}" + ) + except github3.exceptions.UnprocessableEntity as e: + self.logger.error( + f"Error creating merge conflict pull request to merge {source} into {branch_name}:\n{e.response.text}" + ) + def _is_source_branch_direct_descendent(self, branch_name): """Returns True if branch is a direct descendent of the source branch""" source_dunder_count = self.options["source_branch"].count("__") diff --git a/cumulusci/tasks/github/tests/test_merge.py b/cumulusci/tasks/github/tests/test_merge.py index f84b62f296..80743f7cb3 100644 --- a/cumulusci/tasks/github/tests/test_merge.py +++ b/cumulusci/tasks/github/tests/test_merge.py @@ -278,6 +278,45 @@ def test_task_output__feature_branch_merge_conflict(self): assert expected_log == actual_log assert 7 == len(responses.calls) + @responses.activate + def test_task_output__feature_branch_merge_conflict_skip_pull_requests(self): + self._mock_repo() + self._mock_branch(self.branch) + self.mock_pulls() + branch_name = "feature/a-test" + branches = [] + branches.append(self._get_expected_branch("main")) + branches.append(self._get_expected_branch(branch_name)) + branches = self._mock_branches(branches) + self._mock_compare( + base=branches[1]["name"], + head=self.project_config.repo_commit, + files=[{"filename": "test.txt"}], + ) + self._mock_merge(http.client.CONFLICT) + self._mock_pull_create(1, 2) + with LogCapture() as log: + task = self._create_task( + task_config={ + "options": { + "create_pull_request_on_conflict": "False", + } + } + ) + task() + actual_log = self._get_log_lines(log) + + expected_log = log_header() + [ + ("DEBUG", "Skipping branch main: is source branch"), + ("DEBUG", "Found descendents of main to update: ['feature/a-test']"), + ( + "INFO", + "Merge conflict on branch feature/a-test: skipping pull request creation", + ), + ] + assert expected_log == actual_log + assert 5 == len(responses.calls) + @responses.activate def test_merge__error_on_merge_conflict_pr(self): self._mock_repo() @@ -402,6 +441,59 @@ def test_task_output__main_parent_with_child_pr(self): assert expected_log == actual_log assert 7 == len(responses.calls) + @responses.activate + def test_task_output__main_parent_with_child_skip_pull_requests(self): + self._mock_repo() + self._mock_branch(self.branch) + # branches + parent_branch_name = "feature/a-test" + child_branch_name = "feature/a-test__a-child" + branches = [] + branches.append(self._get_expected_branch("main")) + branches.append(self._get_expected_branch(parent_branch_name)) + branches.append(self._get_expected_branch(child_branch_name)) + branches = self._mock_branches(branches) + # pull request + pull = self._get_expected_pull_request(1, 2) + pull["base"]["ref"] = parent_branch_name + pull["base"]["sha"] = branches[1]["commit"]["sha"] + pull["head"]["ref"] = child_branch_name + self.mock_pulls(pulls=[pull]) + # compare + self._mock_compare( + base=parent_branch_name, + head=self.project_config.repo_commit, + files=[{"filename": "test.txt"}], + ) + # merge + self._mock_merge(http.client.CONFLICT) + # create PR + self._mock_pull_create(1, 2) + with LogCapture() as log: + task = self._create_task( + task_config={ + "options": { + "create_pull_request_on_conflict": "False", + } + } + ) + task() + actual_log = self._get_log_lines(log) + expected_log = log_header() + [ + ("DEBUG", "Skipping branch main: is source branch"), + ( + "DEBUG", + "Skipping branch feature/a-test__a-child: is not a direct descendent of main", + ), + ("DEBUG", "Found descendents of main to update: ['feature/a-test']"), + ( + "INFO", + "Merge conflict on branch feature/a-test: skipping pull request creation", + ), + ] + assert expected_log == actual_log + assert 5 == len(responses.calls) + @responses.activate def test_task_output__main_merge_to_feature(self): """Tests that commits to the main branch are merged to the expected feature branches""" From e39619d58b82264ff78954563a21c6afb08b6b59 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Mon, 7 Aug 2023 09:09:24 -0700 Subject: [PATCH 10/98] fix: Don't pass fullName for Unlocked packages (#3636) This commit fixes an issue where the creation of a beta version of an unlocked package was failing during the `create_package_version` task with a misleading error message about a 1GP dependency. The workaround is to match the behavior of the [@salesforce/packaging] typescript library: when the package type is 'unlocked', 'None' is passed instead of the package name. Fixes #3633 [@salesforce/packaging]: https://github.com/forcedotcom/packaging/tree/830d45bd52b80b09b7a5f96b9664961eee3ce7f1 --- cumulusci/tasks/create_package_version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index a3fa39def3..c9a08ca72e 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -211,7 +211,9 @@ def _run_task(self): package_zip_builder = None with convert_sfdx_source( self.project_config.default_package_path, - self.package_config.package_name, + None + if self.package_config.package_type == PackageTypeEnum.unlocked + else self.package_config.package_name, self.logger, ) as path: package_zip_builder = MetadataPackageZipBuilder( From 9ccf3c9566f78c6e9102ac214db30470cef660c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 20:52:08 +0000 Subject: [PATCH 11/98] Release v3.78.0 (#3638) Co-authored-by: github-actions Co-authored-by: Josh Kofsky Co-authored-by: David Reed --- cumulusci/__about__.py | 2 +- docs/history.md | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 418e04e00b..b02e77d7ea 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.77.0" +__version__ = "3.78.0" diff --git a/docs/history.md b/docs/history.md index 2fe6a30c78..a7784105ab 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,26 @@ +## v3.78.0 (2023-08-10) + + + +## What's Changed + +### Changes 🎉 + +- Updated Permission Set preflights to handle Permission Set Groups by [@jkasturi-sf](https://github.com/jkasturi-sf) in [#3623](https://github.com/SFDO-Tooling/CumulusCI/pull/3623) +- Added the `create_pull_request_on_conflict` option to automerge tasks by [@BenjaminFrench](https://github.com/ BenjaminFrench) in [#3632](https://github.com/SFDO-Tooling/CumulusCI/pull/3632) + +### Issues Fixed 🩴 + +- Fixed issues with uploading some Unlocked Packages by [@jstvz](https://github.com/jstvz) in [#3636](https://github.com/SFDO-Tooling/CumulusCI/pull/3636) +- Corrected behavior of the `skip_future_releases` option by [@jstvz](https://github.com/jstvz) in [#3624](https://github.com/SFDO-Tooling/CumulusCI/pull/3624) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.77.0...v3.78.0 + + + ## v3.77.0 (2023-07-07) @@ -15,8 +35,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.76.0...v3.77.0 - - ## v3.76.0.dev0 (2023-05-25) From 371a94235726f606fc9499d22917f3796c1930a0 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Tue, 29 Aug 2023 12:06:59 -0700 Subject: [PATCH 12/98] Add workarounds to support Vlocity local comp in CI (#3642) We discovered that using sf.accessToken for persistent orgs is not compatible with the local compilation of omniscripts, which requires this workaround. Moreover, vlocity_build does not support passing the NPM auth token via CLI or environment variable, so we're adding support for this to the vlocity_pack_deploy task. --- cumulusci/tasks/vlocity/tests/test_vlocity.py | 44 +++++++- cumulusci/tasks/vlocity/vlocity.py | 100 ++++++++++++++++-- 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/cumulusci/tasks/vlocity/tests/test_vlocity.py b/cumulusci/tasks/vlocity/tests/test_vlocity.py index 09309a20e4..7fa45536ad 100644 --- a/cumulusci/tasks/vlocity/tests/test_vlocity.py +++ b/cumulusci/tasks/vlocity/tests/test_vlocity.py @@ -12,6 +12,9 @@ BUILD_TOOL_MISSING_ERROR, LWC_RSS_NAME, OMNI_NAMESPACE, + SF_TOKEN_ENV, + VBT_SF_ALIAS, + VBT_TOKEN_ENV, VF_LEGACY_RSS_NAME, VF_RSS_NAME, OmniStudioDeployRemoteSiteSettings, @@ -67,7 +70,7 @@ persistent_org_config, VlocityRetrieveTask, None, - f"vlocity packExport -job vlocity.yaml -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'", + f"vlocity packExport -job vlocity.yaml -sfdx.username '{VBT_SF_ALIAS}'", ), ( scratch_org_config, @@ -79,13 +82,13 @@ persistent_org_config, VlocityDeployTask, None, - f"vlocity packDeploy -job vlocity.yaml -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}'", + f"vlocity packDeploy -job vlocity.yaml -sfdx.username '{VBT_SF_ALIAS}'", ), ( persistent_org_config, VlocityDeployTask, "foo=bar", - f"vlocity packDeploy -job vlocity.yaml -sf.accessToken '{access_token}' -sf.instanceUrl '{instance_url}' foo=bar", + f"vlocity packDeploy -job vlocity.yaml -sfdx.username '{VBT_SF_ALIAS}' foo=bar", ), ] @@ -96,7 +99,6 @@ def test_vlocity_simple_job( project_config, org_config, task_class, extra, expected_command ): - task_config = TaskConfig( config={ "options": { @@ -108,7 +110,16 @@ def test_vlocity_simple_job( ) task = task_class(project_config, task_config, org_config) - assert task._get_command() == expected_command + with mock.patch( + "cumulusci.tasks.vlocity.vlocity.sarge.Command", + ) as Command: + assert task._get_command() == expected_command + if not isinstance(org_config, ScratchOrgConfig): + cmd_args, cmd_kwargs = Command.call_args + assert SF_TOKEN_ENV in cmd_kwargs["env"] + assert cmd_kwargs["env"][SF_TOKEN_ENV] == access_token + assert instance_url in cmd_args[0] + assert VBT_SF_ALIAS in cmd_args[0] def test_vlocity_build_tool_missing(project_config): @@ -125,6 +136,29 @@ def test_vlocity_build_tool_missing(project_config): task._init_task() +def test_vlocity_npm_auth_env(project_config, tmp_path, monkeypatch): + job_file = tmp_path / "vlocity.yaml" + job_file.write_text("key: value") + task_config = TaskConfig( + config={"options": {"job_file": job_file, "org": org_name}} + ) + task = VlocityDeployTask(project_config, task_config, scratch_org_config) + # No env, don't write + failure: bool = task.add_npm_auth_to_jobfile(str(job_file), VBT_TOKEN_ENV) + assert failure is False + + monkeypatch.setenv(VBT_TOKEN_ENV, "token") + success: bool = task.add_npm_auth_to_jobfile(str(job_file), VBT_TOKEN_ENV) + job_file_txt = job_file.read_text() + assert success is True + assert "npmAuthKey: token" in job_file_txt + + skipped: bool = task.add_npm_auth_to_jobfile(str(job_file), VBT_TOKEN_ENV) + job_file_skip = job_file.read_text() + assert skipped is False + assert job_file_skip == job_file_txt + + namespace = "omnistudio" test_cases = [ (TaskConfig(config={}), OMNI_NAMESPACE), diff --git a/cumulusci/tasks/vlocity/vlocity.py b/cumulusci/tasks/vlocity/vlocity.py index 7eb1386958..2071a61c83 100644 --- a/cumulusci/tasks/vlocity/vlocity.py +++ b/cumulusci/tasks/vlocity/vlocity.py @@ -1,11 +1,14 @@ +import os import re import sys from abc import ABC +from pathlib import Path from typing import Final import sarge from cumulusci.core.config.scratch_org_config import ScratchOrgConfig +from cumulusci.core.exceptions import SfdxOrgException from cumulusci.core.tasks import BaseSalesforceTask from cumulusci.tasks.command import Command from cumulusci.tasks.metadata_etl.remote_site_settings import ( @@ -23,6 +26,9 @@ VF_LEGACY_RSS_NAME = "OmniStudioLegacyVisualforce" LWC_RSS_NAME = "OmniStudioLightning" OMNI_NAMESPACE = "omnistudio" +VBT_SF_ALIAS = "cci-vbt-target" +SF_TOKEN_ENV = "SFDX_ACCESS_TOKEN" +VBT_TOKEN_ENV = "OMNIOUT_TOKEN" class VlocityBaseTask(Command, BaseSalesforceTask): @@ -80,16 +86,41 @@ def _get_command(self) -> str: command: str = f"{self.command_keyword} -job {job_file}" - if isinstance(self.org_config, ScratchOrgConfig): - command = f"{command} -sfdx.username '{username}'" - else: - access_token: str = f"-sf.accessToken '{self.org_config.access_token}'" - instance_url: str = f"-sf.instanceUrl '{self.org_config.instance_url}'" - command = f"{command} {access_token} {instance_url}" + if not isinstance(self.org_config, ScratchOrgConfig): + username = self._add_token_to_sfdx( + self.org_config.access_token, self.org_config.instance_url + ) + command = f"{command} -sfdx.username '{username}'" self.options["command"] = command return super()._get_command() + def _add_token_to_sfdx(self, access_token: str, instance_url: str) -> str: + """ + HACK: VBT's documentation suggests that passing sf.accessToken/sf.instanceUrl + is compatible with local compilation, but our experience (as of VBT v1.17) + says otherwise. This function is our workaround: by adding the access token + and temporarily setting it as the default we allow VBT to deploy the + locally compiled components via SFDX or salesforce-alm. + """ + # TODO: Use the sf v2 form of this command instead (when we migrate) + token_store_cmd = [ + "sfdx", + "force:auth:accesstoken:store", + "--no-prompt", + "--alias", + f"{VBT_SF_ALIAS}", + "--instance-url", + f"{instance_url}", + ] + try: + p = sarge.Command(token_store_cmd, env={SF_TOKEN_ENV: access_token}) + p.run(async_=True) + p.wait() + except Exception as exc: + raise SfdxOrgException("token store failed") from exc + return VBT_SF_ALIAS + class VlocityRetrieveTask(VlocitySimpleJobTask): """Runs a `vlocity packExport` command with a given user and job file""" @@ -102,6 +133,63 @@ class VlocityDeployTask(VlocitySimpleJobTask): command_keyword: Final[str] = "packDeploy" + task_options: dict = { + "job_file": { + "description": "Filepath to the jobfile", + "required": True, + }, + "extra": {"description": "Any extra arguments to pass to the vlocity CLI"}, + "npm_auth_key_env": { + "description": ( + "Environment variable storing an auth token for the " + "Vlocity NPM Repository (npmAuthKey). If defined, appended " + "to the job file." + ), + "default": VBT_TOKEN_ENV, + }, + } + + def _get_command(self) -> str: + npm_env_var: str = self.options.get("npm_auth_key_env", VBT_TOKEN_ENV) + job_file: str = self.options.get("job_file") + + self.add_npm_auth_to_jobfile(job_file, npm_env_var) + + return super()._get_command() + + def add_npm_auth_to_jobfile(self, job_file: str, npm_env_var: str) -> bool: + """ + HACK: VBT local compilation requires use of an auth token for a private + NPM repository, defined as the npmAuthKey option. Unfortunately, this + option can't be defined in an environment variable, nor can it be passed + via the CLI. Instead, the option is _only_ read from the job file, so the + secret must be committed to source control for CI/CD. Our workaround is to + check for the presence of npm_env_var in the environment, and appending it + to the job file if it is. + + Retuns: + - False: No environment variable found or conflict exists in job file + - True: Auth token written to job file + """ + found_msg = f"VBT NPM environment variable named '{npm_env_var}' found." + if npm_key := os.environ.get(npm_env_var): + self.logger.info(found_msg) + else: + self.logger.debug(f"No {found_msg}") + return False + + job_file_path: Path = Path(job_file) + job_file_txt = job_file_path.read_text() + + # Warn about duplicate keys to avoid showing the user a nasty JS traceback + if "npmAuthKey" in job_file_txt: + self.logger.warning("npmAuthKey present in job file, skipping...") + return False + + self.logger.info(f"Appending to {job_file}") + job_file_path.write_text(f"{job_file_txt}\nnpmAuthKey: {npm_key}") + return True + class OmniStudioDeployRemoteSiteSettings(AddRemoteSiteSettings): """Deploys remote site settings needed for OmniStudio. From 8e6fa8aeb74ba184a7db0cd589c7c2f791318f71 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 15:31:11 -0700 Subject: [PATCH 13/98] Release v3.79.0 (#3647) * Update changelog (automated) * Update changelog --------- Co-authored-by: github-actions Co-authored-by: James Estevez --- cumulusci/__about__.py | 2 +- docs/history.md | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index b02e77d7ea..907f89c800 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.78.0" +__version__ = "3.79.0" diff --git a/docs/history.md b/docs/history.md index a7784105ab..66b75c1644 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,20 @@ +## v3.79.0 (2023-09-07) + + + +## What's Changed + +### Changes 🎉 + +- Added workarounds to support Vlocity local compilation in CI by [@jstvz](https://github.com/jstvz) in [#3642](https://github.com/SFDO-Tooling/CumulusCI/pull/3642) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.78.0...v3.79.0 + + + ## v3.78.0 (2023-08-10) @@ -20,8 +34,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.77.0...v3.78.0 - - ## v3.77.0 (2023-07-07) From 649e30632de625b47744ba9e9384ffc3784f15d0 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 09:19:06 +0530 Subject: [PATCH 14/98] Adding Functionslity --- cumulusci/cli/org.py | 14 ++++++++++++-- cumulusci/core/config/scratch_org_config.py | 7 +++++++ cumulusci/core/keychain/base_project_keychain.py | 5 ++++- cumulusci/cumulusci.yml | 1 + cumulusci/schema/cumulusci.jsonschema.json | 6 ++++++ cumulusci/utils/yaml/cumulusci_yml.py | 1 + 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 4dc5ed3e6f..e16e493ffc 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -546,6 +546,7 @@ def org_remove(runtime, org_name, global_org): name="scratch", help="Connects a Salesforce DX Scratch Org to the keychain" ) @click.argument("config_name") +@click.argument("release") @orgname_option_or_argument(required=True) @click.option( "--default", @@ -562,9 +563,18 @@ def org_remove(runtime, org_name, global_org): @click.option( "--no-password", is_flag=True, help="If set, don't set a password for the org" ) +# @click.option( +# "--release",help="If provided specify the release when creating a scratch org" +# ) @pass_runtime(require_keychain=True) -def org_scratch(runtime, config_name, org_name, default, devhub, days, no_password): +def org_scratch( + runtime, config_name, org_name, default, devhub, days, no_password, release +): runtime.check_org_overwrite(org_name) + release_options = ["previous", "preview"] + if release and release not in release_options: + raise click.UsageError("Release options value is not valid.") + # Test for Pydanctic validation of release scratch_configs = runtime.project_config.lookup("orgs__scratch") if not scratch_configs: @@ -579,7 +589,7 @@ def org_scratch(runtime, config_name, org_name, default, devhub, days, no_passwo scratch_config["devhub"] = devhub runtime.keychain.create_scratch_org( - org_name, config_name, days, set_password=not (no_password) + org_name, config_name, days, set_password=not (no_password), release=release ) if default: diff --git a/cumulusci/core/config/scratch_org_config.py b/cumulusci/core/config/scratch_org_config.py index 4f2b5766f2..921d2b0fb0 100644 --- a/cumulusci/core/config/scratch_org_config.py +++ b/cumulusci/core/config/scratch_org_config.py @@ -38,6 +38,10 @@ def scratch_info(self): def days(self) -> int: return self.config.setdefault("days", 1) + @property + def release(self) -> str: + return self.config.setdefault("release", None) + @property def active(self) -> bool: """Check if an org is alive""" @@ -123,6 +127,7 @@ def raise_error() -> NoReturn: def _build_org_create_args(self) -> List[str]: args = ["-f", self.config_file, "-w", "120"] + devhub_username: Optional[str] = self._choose_devhub_username() if devhub_username: args += ["--targetdevhubusername", devhub_username] @@ -132,6 +137,8 @@ def _build_org_create_args(self) -> List[str]: args += ["--noancestors"] if self.days: args += ["--durationdays", str(self.days)] + if self.release: + args += [f"release={self.release}"] if self.sfdx_alias: args += ["-a", self.sfdx_alias] with open(self.config_file, "r") as org_def: diff --git a/cumulusci/core/keychain/base_project_keychain.py b/cumulusci/core/keychain/base_project_keychain.py index 78f40a9035..0ab9a0af8e 100644 --- a/cumulusci/core/keychain/base_project_keychain.py +++ b/cumulusci/core/keychain/base_project_keychain.py @@ -54,7 +54,9 @@ def _validate_key(self): # Orgs # ####################################### - def create_scratch_org(self, org_name, config_name, days=None, set_password=True): + def create_scratch_org( + self, org_name, config_name, days=None, set_password=True, release=None + ): """Adds/Updates a scratch org config to the keychain from a named config""" scratch_config = self.project_config.lookup(f"orgs__scratch__{config_name}") if scratch_config is None: @@ -69,6 +71,7 @@ def create_scratch_org(self, org_name, config_name, days=None, set_password=True scratch_config["scratch"] = True scratch_config.setdefault("namespaced", False) scratch_config["config_name"] = config_name + scratch_config["release"] = release scratch_config[ "sfdx_alias" ] = f"{self.project_config.project__name}__{org_name}" diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 57cd1e0220..f8cefe776c 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -641,6 +641,7 @@ tasks: options: name: Release production: True + namespace: foodPackage group: Release Operations upload_user_profile_photo: group: Salesforce Users diff --git a/cumulusci/schema/cumulusci.jsonschema.json b/cumulusci/schema/cumulusci.jsonschema.json index 7d53f2c257..0331eb3c3f 100644 --- a/cumulusci/schema/cumulusci.jsonschema.json +++ b/cumulusci/schema/cumulusci.jsonschema.json @@ -430,6 +430,12 @@ "noancestors": { "title": "Noancestors", "type": "boolean" + }, + "release": { + "title": "Release", + "default": "previous", + "enum": ["previous", "preview"], + "type": "string" } }, "additionalProperties": false diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index a85cee77bd..d1b9851164 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -150,6 +150,7 @@ class ScratchOrg(CCIDictModel): namespaced: str = None setup_flow: str = None noancestors: bool = None + release: Literal["previous", "preview"] = "previous" class Orgs(CCIDictModel): From 1c07efbddc206ee53c99a4b7819e7aeb2f043edc Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 11:28:40 +0530 Subject: [PATCH 15/98] Wrote Tests --- cumulusci/cli/org.py | 8 +++----- cumulusci/cli/tests/test_org.py | 23 ++++++++++++++++++++++- cumulusci/utils/yaml/cumulusci_yml.py | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index e16e493ffc..dd5ebb133a 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -546,7 +546,6 @@ def org_remove(runtime, org_name, global_org): name="scratch", help="Connects a Salesforce DX Scratch Org to the keychain" ) @click.argument("config_name") -@click.argument("release") @orgname_option_or_argument(required=True) @click.option( "--default", @@ -563,9 +562,9 @@ def org_remove(runtime, org_name, global_org): @click.option( "--no-password", is_flag=True, help="If set, don't set a password for the org" ) -# @click.option( -# "--release",help="If provided specify the release when creating a scratch org" -# ) +@click.option( + "--release", help="If provided specify the release when creating a scratch org" +) @pass_runtime(require_keychain=True) def org_scratch( runtime, config_name, org_name, default, devhub, days, no_password, release @@ -574,7 +573,6 @@ def org_scratch( release_options = ["previous", "preview"] if release and release not in release_options: raise click.UsageError("Release options value is not valid.") - # Test for Pydanctic validation of release scratch_configs = runtime.project_config.lookup("orgs__scratch") if not scratch_configs: diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index 01692e22a8..5b55b66f8f 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -1247,14 +1247,35 @@ def test_org_scratch(self): devhub="hub", days=7, no_password=True, + release="previous", ) runtime.check_org_overwrite.assert_called_once() runtime.keychain.create_scratch_org.assert_called_with( - "test", "dev", 7, set_password=False + "test", "dev", 7, set_password=False, release="previous" ) runtime.keychain.set_default_org.assert_called_with("test") + def test_org_scratch_release_invalid(self): + runtime = mock.Mock() + + runtime.project_config.lookup = MockLookup( + orgs__scratch={"dev": {"orgName": "Dev"}} + ) + with pytest.raises(click.UsageError): + run_click_command( + org.org_scratch, + runtime=runtime, + config_name="dev", + org_name="test", + default=True, + devhub="hub", + days=7, + no_password=True, + release="next", + ) + runtime.check_org_overwrite.assert_called_once() + def test_org_scratch__not_default(self): runtime = mock.Mock() runtime.project_config.lookup = MockLookup( diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index d1b9851164..2a2a63bc86 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -150,7 +150,7 @@ class ScratchOrg(CCIDictModel): namespaced: str = None setup_flow: str = None noancestors: bool = None - release: Literal["previous", "preview"] = "previous" + # release: Literal["previous", "preview"] = "previous" class Orgs(CCIDictModel): From 0047bfb41c62e6e925139cf8dd7786cf039f824d Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 14:30:49 +0530 Subject: [PATCH 16/98] Test Fixes --- cumulusci/cli/tests/test_org.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index 5b55b66f8f..6b2249fd62 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -1291,11 +1291,12 @@ def test_org_scratch__not_default(self): devhub="hub", days=7, no_password=True, + release="previous", ) runtime.check_org_overwrite.assert_called_once() runtime.keychain.create_scratch_org.assert_called_with( - "test", "dev", 7, set_password=False + "test", "dev", 7, set_password=False, release="previous" ) def test_org_scratch_no_configs(self): @@ -1312,6 +1313,7 @@ def test_org_scratch_no_configs(self): devhub="hub", days=7, no_password=True, + release="previous", ) def test_org_scratch_config_not_found(self): @@ -1328,6 +1330,7 @@ def test_org_scratch_config_not_found(self): devhub="hub", days=7, no_password=True, + release="previous", ) def test_org_scratch_delete(self): From 6c6025280324dc35e4dc4713b6f46c6f5241b2ca Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 16:40:39 +0530 Subject: [PATCH 17/98] Cleanup --- cumulusci/cli/org.py | 2 +- cumulusci/cli/tests/test_org.py | 4 ++-- cumulusci/core/config/scratch_org_config.py | 1 - cumulusci/cumulusci.yml | 1 - cumulusci/utils/yaml/cumulusci_yml.py | 3 +-- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index dd5ebb133a..0df0644f24 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -563,7 +563,7 @@ def org_remove(runtime, org_name, global_org): "--no-password", is_flag=True, help="If set, don't set a password for the org" ) @click.option( - "--release", help="If provided specify the release when creating a scratch org" + "--release", help="If provided, specify either previous or preview when creating a scratch org" ) @pass_runtime(require_keychain=True) def org_scratch( diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index 6b2249fd62..8a83998890 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -1291,12 +1291,12 @@ def test_org_scratch__not_default(self): devhub="hub", days=7, no_password=True, - release="previous", + release=None, ) runtime.check_org_overwrite.assert_called_once() runtime.keychain.create_scratch_org.assert_called_with( - "test", "dev", 7, set_password=False, release="previous" + "test", "dev", 7, set_password=False, release=None ) def test_org_scratch_no_configs(self): diff --git a/cumulusci/core/config/scratch_org_config.py b/cumulusci/core/config/scratch_org_config.py index 921d2b0fb0..17a845fe63 100644 --- a/cumulusci/core/config/scratch_org_config.py +++ b/cumulusci/core/config/scratch_org_config.py @@ -127,7 +127,6 @@ def raise_error() -> NoReturn: def _build_org_create_args(self) -> List[str]: args = ["-f", self.config_file, "-w", "120"] - devhub_username: Optional[str] = self._choose_devhub_username() if devhub_username: args += ["--targetdevhubusername", devhub_username] diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index f8cefe776c..57cd1e0220 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -641,7 +641,6 @@ tasks: options: name: Release production: True - namespace: foodPackage group: Release Operations upload_user_profile_photo: group: Salesforce Users diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index 2a2a63bc86..b0aa16d897 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -149,8 +149,7 @@ class ScratchOrg(CCIDictModel): days: int = None namespaced: str = None setup_flow: str = None - noancestors: bool = None - # release: Literal["previous", "preview"] = "previous" + noancestors: bool = None class Orgs(CCIDictModel): From 6879a22b40403a31776668463630c79b90389f6c Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 16:47:01 +0530 Subject: [PATCH 18/98] Cleanup --- cumulusci/cli/org.py | 4 +++- cumulusci/utils/yaml/cumulusci_yml.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 0df0644f24..0340ff6728 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -563,13 +563,15 @@ def org_remove(runtime, org_name, global_org): "--no-password", is_flag=True, help="If set, don't set a password for the org" ) @click.option( - "--release", help="If provided, specify either previous or preview when creating a scratch org" + "--release", + help="If provided, specify either previous or preview when creating a scratch org", ) @pass_runtime(require_keychain=True) def org_scratch( runtime, config_name, org_name, default, devhub, days, no_password, release ): runtime.check_org_overwrite(org_name) + release_options = ["previous", "preview"] if release and release not in release_options: raise click.UsageError("Release options value is not valid.") diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index b0aa16d897..a85cee77bd 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -149,7 +149,7 @@ class ScratchOrg(CCIDictModel): days: int = None namespaced: str = None setup_flow: str = None - noancestors: bool = None + noancestors: bool = None class Orgs(CCIDictModel): From cb511004481341002d92638605d371904543c1e0 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 16:49:26 +0530 Subject: [PATCH 19/98] Cleanup --- cumulusci/schema/cumulusci.jsonschema.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cumulusci/schema/cumulusci.jsonschema.json b/cumulusci/schema/cumulusci.jsonschema.json index 0331eb3c3f..7d53f2c257 100644 --- a/cumulusci/schema/cumulusci.jsonschema.json +++ b/cumulusci/schema/cumulusci.jsonschema.json @@ -430,12 +430,6 @@ "noancestors": { "title": "Noancestors", "type": "boolean" - }, - "release": { - "title": "Release", - "default": "previous", - "enum": ["previous", "preview"], - "type": "string" } }, "additionalProperties": false From 27c7874501e3d1273705c4938ef2af7dd406a03b Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 19 Sep 2023 16:55:37 +0530 Subject: [PATCH 20/98] Cleamup --- cumulusci/cli/org.py | 1 - cumulusci/cli/tests/test_org.py | 1 - cumulusci/core/keychain/base_project_keychain.py | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 0340ff6728..3d7514d348 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -571,7 +571,6 @@ def org_scratch( runtime, config_name, org_name, default, devhub, days, no_password, release ): runtime.check_org_overwrite(org_name) - release_options = ["previous", "preview"] if release and release not in release_options: raise click.UsageError("Release options value is not valid.") diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index 8a83998890..bf9656b7cc 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -1281,7 +1281,6 @@ def test_org_scratch__not_default(self): runtime.project_config.lookup = MockLookup( orgs__scratch={"dev": {"orgName": "Dev"}} ) - run_click_command( org.org_scratch, runtime=runtime, diff --git a/cumulusci/core/keychain/base_project_keychain.py b/cumulusci/core/keychain/base_project_keychain.py index 0ab9a0af8e..92b2a1278d 100644 --- a/cumulusci/core/keychain/base_project_keychain.py +++ b/cumulusci/core/keychain/base_project_keychain.py @@ -78,6 +78,7 @@ def create_scratch_org( org_config = ScratchOrgConfig( scratch_config, org_name, keychain=self, global_org=False ) + org_config.save() def set_org(self, org_config, global_org=False, save=True): From 87b94440e62ea53429f2e54f89ff7d2711b03e7e Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Tue, 19 Sep 2023 22:29:22 +0530 Subject: [PATCH 21/98] Deploy Major and Minor Version option in upload_production task (#3651) --- cumulusci/tasks/salesforce/package_upload.py | 68 ++++++- .../salesforce/tests/test_PackageUpload.py | 185 +++++++++++++++++- 2 files changed, 251 insertions(+), 2 deletions(-) diff --git a/cumulusci/tasks/salesforce/package_upload.py b/cumulusci/tasks/salesforce/package_upload.py index 4c38646116..f58029ee68 100644 --- a/cumulusci/tasks/salesforce/package_upload.py +++ b/cumulusci/tasks/salesforce/package_upload.py @@ -2,7 +2,11 @@ from cumulusci.cli.ui import CliTable from cumulusci.core.dependencies.resolvers import get_static_dependencies -from cumulusci.core.exceptions import ApexTestException, SalesforceException +from cumulusci.core.exceptions import ( + ApexTestException, + SalesforceException, + TaskOptionsError, +) from cumulusci.tasks.salesforce import BaseSalesforceApiTask @@ -32,6 +36,14 @@ class PackageUpload(BaseSalesforceApiTask): "resolution_strategy": { "description": "The name of a sequence of resolution_strategy (from project__dependency_resolutions) to apply to dynamic dependencies. Defaults to 'production'." }, + "major_version": { + "description": "The desired major version number for the uploaded package. Defaults to latest major version.", + "required": False, + }, + "minor_version": { + "description": "The desired minor version number for the uploaded package. Defaults to next available minor version for the current major version.", + "required": False, + }, } def _init_options(self, kwargs): @@ -46,7 +58,10 @@ def _init_options(self, kwargs): self.options["namespace"] = self.project_config.project__package__namespace def _run_task(self): + + self._validate_versions() self._set_package_id() + self._set_package_info() self._make_package_upload_request() @@ -59,6 +74,54 @@ def _run_task(self): self._set_dependencies() self._log_package_upload_success() + def _validate_versions(self): + """This functions validates the major version and minor version if passed and sets them in the options dictionary if not passed.""" + + # Fetching the latest major,minor and patch version from Database + version = self._get_one_record( + ( + """ + SELECT MajorVersion, + MinorVersion, + PatchVersion, + ReleaseState + FROM MetadataPackageVersion + ORDER BY + MajorVersion DESC, + MinorVersion DESC, + PatchVersion DESC + LIMIT 1 + """ + ), + "Version not found", + ) + + # This if-else condition updates the major version to latest major version in options if not passed via command line and validates it if passed. + if "major_version" in self.options: + try: + if int(self.options["major_version"]) < version["MajorVersion"]: + raise TaskOptionsError("Major Version not valid.") + except ValueError: + raise TaskOptionsError("Major Version not valid.") + + else: + self.options["major_version"] = str(version["MajorVersion"]) + + # This if is executed only when major version is equal to latest major version. Updates the minor version in options if not passed and validates if passed + # Updates minor version when not passed in remaining cases. + if self.options["major_version"] == str(version["MajorVersion"]): + if "minor_version" in self.options: + if int(self.options["minor_version"]) <= version["MinorVersion"]: + raise TaskOptionsError("Minor Version not valid.") + else: + if version["ReleaseState"] == "Beta": + self.options["minor_version"] = str(version["MinorVersion"]) + else: + self.options["minor_version"] = str(version["MinorVersion"] + 1) + else: + if "minor_version" not in self.options: + self.options["minor_version"] = "0" + def _set_package_info(self): if not self.package_id: self._set_package_id() @@ -69,6 +132,8 @@ def _set_package_info(self): "VersionName": self.options["name"], "IsReleaseVersion": production, "MetadataPackageId": self.package_id, + "MajorVersion": self.options["major_version"], + "MinorVersion": self.options["minor_version"], } if "description" in self.options: @@ -208,6 +273,7 @@ def _set_package_version_values_on_self(self): f"Version {self.version_id} not found", ) version_parts = [str(version["MajorVersion"]), str(version["MinorVersion"])] + if version["PatchVersion"]: version_parts.append(str(version["PatchVersion"])) diff --git a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py index f33be0c16c..c98a2e7970 100644 --- a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py +++ b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py @@ -6,7 +6,11 @@ from cumulusci.core.config.org_config import OrgConfig, VersionInfo from cumulusci.core.config.project_config import BaseProjectConfig from cumulusci.core.config.universal_config import UniversalConfig -from cumulusci.core.exceptions import ApexTestException, SalesforceException +from cumulusci.core.exceptions import ( + ApexTestException, + SalesforceException, + TaskOptionsError, +) from cumulusci.tasks.salesforce import PackageUpload from .util import create_task @@ -22,6 +26,8 @@ def test_run_task(self): "password": "pw", "post_install_url": "http://www.salesforce.org", "release_notes_url": "https://github.com", + "major_version": "2", + "minor_version": "11", }, ) @@ -29,6 +35,18 @@ def _init_class(): task.tooling = mock.Mock( query=mock.Mock( side_effect=[ + # Query for latest major and minor version used via validate version function + { + "totalSize": 1, + "records": [ + { + "MajorVersion": 1, + "MinorVersion": 0, + "PatchVersion": 1, + "ReleaseState": "Released", + } + ], + }, # Query for package by namespace {"totalSize": 1, "records": [{"Id": "PKG_ID"}]}, # Query for upload status @@ -75,6 +93,155 @@ def test_set_package_id(self): task._set_package_id() assert expected_package_id == task.package_id + test_validate_version_base_options = { + "name": "Test Release", + "production": False, + "description": "Test Description", + "password": "secret", + "post_install_url": "post.install.url", + "release_notes_url": "release.notes.url", + } + + def generate_valid_version_options( + major_version: str, + minor_version: str, + asserted_major_version: str, + asserted_minor_version: str, + is_negative: bool = False, + ): + + """This is function is used to generate test cases for test_validate_versions(positive as well as negative) + Arguments: + major_version:acutal major version which is to be passed none if not passed. + minor_version:acutal minor version which is to be passed none if not passed + asserted_major_version:major version which is expected in options dict after running of validate version function. + asserted_minor_version:minor version which is expected in options dict after running of validate version function. + is_negative:This boolean is used to decide whether the function returns a postive or negative test case. defaults to true is not passed. + """ + test_validate_version_base_options = { + "name": "Test Release", + "production": False, + "description": "Test Description", + "password": "secret", + "post_install_url": "post.install.url", + "release_notes_url": "release.notes.url", + } + test_case_actual = { + **test_validate_version_base_options, + } + if major_version is not None: + test_case_actual["major_version"] = major_version + if minor_version is not None: + test_case_actual["minor_version"] = minor_version + + test_case_expected = { + **test_validate_version_base_options, + "major_version": str(asserted_major_version), + "minor_version": str(asserted_minor_version), + } + + if is_negative: + return test_case_actual + return test_case_actual, test_case_expected + + # Generating Positive Test Cases for test_validate_versions + test_positive_options = [ + generate_valid_version_options("1", "2", "1", "2"), + generate_valid_version_options("2", "1", "2", "1"), + generate_valid_version_options(None, "2", "1", "2"), + generate_valid_version_options("1", None, "1", "2"), + generate_valid_version_options("2", None, "2", "0"), + generate_valid_version_options(None, None, "1", "2"), + ] + + @pytest.mark.parametrize("actual_options,expected_options", test_positive_options) + def test_positive_validate_versions(self, actual_options, expected_options): + """Runs Postive Tests for tests_validate_versions""" + task = create_task(PackageUpload, actual_options) + task._get_one_record = mock.Mock( + return_value={ + "MajorVersion": 1, + "MinorVersion": 1, + "PatchVersion": 0, + "ReleaseState": "Released", + } + ) + task._validate_versions() + + assert task.options["name"] == expected_options["name"] + assert task.options["production"] == expected_options["production"] + assert task.options["password"] == expected_options["password"] + assert task.options["post_install_url"] == expected_options["post_install_url"] + assert ( + task.options["release_notes_url"] == expected_options["release_notes_url"] + ) + assert task.options["major_version"] == expected_options["major_version"] + assert task.options["minor_version"] == expected_options["minor_version"] + + def test_positive_validate_versions_for_beta(self): + actual_options = { + "name": "Test Release", + "production": False, + "description": "Test Description", + "password": "secret", + "post_install_url": "post.install.url", + "release_notes_url": "release.notes.url", + "major_version": "1", + } + expected_options = { + "name": "Test Release", + "production": False, + "description": "Test Description", + "password": "secret", + "post_install_url": "post.install.url", + "release_notes_url": "release.notes.url", + "major_version": "1", + "minor_version": "1", + } + task = create_task(PackageUpload, actual_options) + task._get_one_record = mock.Mock( + return_value={ + "MajorVersion": 1, + "MinorVersion": 1, + "PatchVersion": 0, + "ReleaseState": "Beta", + } + ) + task._validate_versions() + + assert task.options["name"] == expected_options["name"] + assert task.options["production"] == expected_options["production"] + assert task.options["password"] == expected_options["password"] + assert task.options["post_install_url"] == expected_options["post_install_url"] + assert ( + task.options["release_notes_url"] == expected_options["release_notes_url"] + ) + assert task.options["major_version"] == expected_options["major_version"] + assert task.options["minor_version"] == expected_options["minor_version"] + + # Generating Negative Test Cases for test_validate_versions + test_negative_options = [ + generate_valid_version_options("0", "2", None, None, True), + generate_valid_version_options("1", "0", None, None, True), + generate_valid_version_options(None, "1", None, None, True), + generate_valid_version_options("ab", 0, None, None, True), + ] + + @pytest.mark.parametrize("actual_options", test_negative_options) + def test_negative_validate_versions(self, actual_options): + """Running Negative Tests for tests_validate_versions""" + task = create_task(PackageUpload, actual_options) + task._get_one_record = mock.Mock( + return_value={ + "MajorVersion": 1, + "MinorVersion": 1, + "PatchVersion": 0, + "ReleaseState": "Released", + } + ) + with pytest.raises(TaskOptionsError): + task._validate_versions() + def test_set_package_info(self): expected_package_id = "12345" options = { @@ -84,6 +251,8 @@ def test_set_package_info(self): "password": "secret", "post_install_url": "post.install.url", "release_notes_url": "release.notes.url", + "major_version": "2", + "minor_version": "2", } task = create_task(PackageUpload, options) @@ -101,6 +270,8 @@ def test_set_package_info(self): assert options["password"] == task.package_info["Password"] assert options["post_install_url"] == task.package_info["PostInstallUrl"] assert options["release_notes_url"] == task.package_info["ReleaseNotesUrl"] + assert options["major_version"] == task.package_info["MajorVersion"] + assert options["minor_version"] == task.package_info["MinorVersion"] @mock.patch("cumulusci.tasks.salesforce.package_upload.datetime") def test_make_package_upload_request(self, datetime): @@ -238,6 +409,18 @@ def _init_class(): task.tooling = mock.Mock( query=mock.Mock( side_effect=[ + # Query for latest major and minor version + { + "totalSize": 1, + "records": [ + { + "MajorVersion": 1, + "MinorVersion": 0, + "PatchVersion": 1, + "ReleaseState": "Released", + } + ], + }, # Query for package by namespace {"totalSize": 1, "records": [{"Id": "PKG_ID"}]}, # Query for upload status From 77a3f4db8d4d4f645570fd14bd49e17bf914698e Mon Sep 17 00:00:00 2001 From: Paul Prescod Date: Tue, 19 Sep 2023 11:17:24 -0700 Subject: [PATCH 22/98] Add better error handling for empty or invalid org and service env vars (#3365) --- cumulusci/core/exceptions.py | 6 +++ .../encrypted_file_project_keychain.py | 22 ++++++++- .../test_encrypted_file_project_keychain.py | 46 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/cumulusci/core/exceptions.py b/cumulusci/core/exceptions.py index 4d8d66f61c..1e1dc2c462 100644 --- a/cumulusci/core/exceptions.py +++ b/cumulusci/core/exceptions.py @@ -62,6 +62,12 @@ class ServiceNotConfigured(CumulusCIUsageError): pass +class ServiceCannotBeLoaded(CumulusCIUsageError): + """Raised when a service cannot be decrypted or unpickled""" + + pass + + class ServiceNotValid(CumulusCIUsageError): """Raised when no service configuration could be found by a given name in the project configuration""" diff --git a/cumulusci/core/keychain/encrypted_file_project_keychain.py b/cumulusci/core/keychain/encrypted_file_project_keychain.py index 58167b49ea..1d4e3d3a64 100644 --- a/cumulusci/core/keychain/encrypted_file_project_keychain.py +++ b/cumulusci/core/keychain/encrypted_file_project_keychain.py @@ -15,6 +15,7 @@ KeychainKeyNotFound, OrgCannotBeLoaded, OrgNotFound, + ServiceCannotBeLoaded, ServiceNotConfigured, ) from cumulusci.core.keychain import BaseProjectKeychain @@ -229,7 +230,16 @@ def _load_orgs_from_environment(self): self._load_org_from_environment(env_var_name, value) def _load_org_from_environment(self, env_var_name, value): - org_config = json.loads(value) + if not value: + raise OrgCannotBeLoaded( + f"Org env var {env_var_name} cannot be loaded because it is empty. Either set {env_var_name} to a json string or unset it from the environment." + ) + try: + org_config = json.loads(value) + except Exception as e: + raise OrgCannotBeLoaded( + f"Could not parse {env_var_name} as JSON becase {e}" + ) org_name = env_var_name[len(self.env_org_var_prefix) :].lower() if org_config.get("scratch"): org_config = scratch_org_factory( @@ -655,6 +665,16 @@ def _load_services_from_environment(self): def _load_service_from_environment(self, env_var_name, value): """Given a valid name/value pair, load the service from the environment on to the keychain""" + if not value: + raise ServiceCannotBeLoaded( + f"Service env var {env_var_name} cannot be loaded because it is empty. Either set {env_var_name} to a json string or unset it from the environment." + ) + try: + service_config = json.loads(value) + except Exception as e: + raise ServiceCannotBeLoaded( + f"Could not parse {env_var_name} as JSON because {e}" + ) service_config = ServiceConfig(json.loads(value)) service_type, service_name = self._get_env_service_type_and_name(env_var_name) self.set_service(service_type, service_name, service_config, save=False) diff --git a/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py b/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py index 4366077d11..9360f71629 100644 --- a/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +++ b/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py @@ -26,7 +26,9 @@ CumulusCIException, CumulusCIUsageError, KeychainKeyNotFound, + OrgCannotBeLoaded, OrgNotFound, + ServiceCannotBeLoaded, ServiceNotConfigured, ServiceNotValid, ) @@ -296,6 +298,28 @@ def test_load_orgs_from_environment(self, keychain, org_config): actual_config = keychain.get_org("devhub") assert _simplify_config(actual_config.config) == org_config.config + def test_load_orgs_from_environment__empty_throws_error(self, keychain, org_config): + env = EnvironmentVarGuard() + with EnvironmentVarGuard() as env: + env.set( + f"{keychain.env_org_var_prefix}dev", + "", + ) + with pytest.raises(OrgCannotBeLoaded): + keychain._load_orgs_from_environment() + + def test_load_orgs_from_environment__invalid_json_throws_error( + self, keychain, org_config + ): + env = EnvironmentVarGuard() + with EnvironmentVarGuard() as env: + env.set( + f"{keychain.env_org_var_prefix}dev", + "['foo',]", + ) + with pytest.raises(OrgCannotBeLoaded): + keychain._load_orgs_from_environment() + ####################################### # Services # ####################################### @@ -358,6 +382,28 @@ def test_load_services_from_env__same_name_throws_error(self, keychain): assert 1 == 1 + def test_load_services_from_env__empty_throws_error(self, keychain): + service_prefix = EncryptedFileProjectKeychain.env_service_var_prefix + env = EnvironmentVarGuard() + with EnvironmentVarGuard() as env: + env.set( + f"{service_prefix}github", + "", + ) + with pytest.raises(ServiceCannotBeLoaded): + keychain._load_services_from_environment() + + def test_load_services_from_env__invalid_json_throws_error(self, keychain): + service_prefix = EncryptedFileProjectKeychain.env_service_var_prefix + env = EnvironmentVarGuard() + with EnvironmentVarGuard() as env: + env.set( + f"{service_prefix}github", + "['foo',]", + ) + with pytest.raises(ServiceCannotBeLoaded): + keychain._load_services_from_environment() + def test_get_service__built_in_connected_app(self, keychain): built_in_connected_app = keychain.get_service("connected_app") assert built_in_connected_app is DEFAULT_CONNECTED_APP From 794bc2f886306c9b23716895233a1c73f532066c Mon Sep 17 00:00:00 2001 From: Paul Prescod Date: Tue, 19 Sep 2023 18:43:02 -0700 Subject: [PATCH 23/98] Remove lines that DG says are an anti-pattern and Python interpreter calls deprecated (#3595) --- cumulusci/core/tests/__init__.py | 1 - cumulusci/salesforce_api/__init__.py | 1 - cumulusci/tasks/__init__.py | 1 - 3 files changed, 3 deletions(-) diff --git a/cumulusci/core/tests/__init__.py b/cumulusci/core/tests/__init__.py index 5284146ebf..e69de29bb2 100644 --- a/cumulusci/core/tests/__init__.py +++ b/cumulusci/core/tests/__init__.py @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) diff --git a/cumulusci/salesforce_api/__init__.py b/cumulusci/salesforce_api/__init__.py index 5284146ebf..e69de29bb2 100644 --- a/cumulusci/salesforce_api/__init__.py +++ b/cumulusci/salesforce_api/__init__.py @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) diff --git a/cumulusci/tasks/__init__.py b/cumulusci/tasks/__init__.py index 5284146ebf..e69de29bb2 100644 --- a/cumulusci/tasks/__init__.py +++ b/cumulusci/tasks/__init__.py @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) From 5d5216ccee8825ed339a9167c7d8285d84aac407 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Tue, 19 Sep 2023 18:53:41 -0700 Subject: [PATCH 24/98] Add guard for empty body to github_release_report (#3645) Fixes #3640. --- cumulusci/tasks/github/release_report.py | 3 ++- .../tasks/github/tests/test_release_report.py | 16 +++++++++++++++- cumulusci/tasks/github/tests/util_github_api.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cumulusci/tasks/github/release_report.py b/cumulusci/tasks/github/release_report.py index b88310653d..bdf104453f 100644 --- a/cumulusci/tasks/github/release_report.py +++ b/cumulusci/tasks/github/release_report.py @@ -69,7 +69,8 @@ def _run_task(self): "time_push_sandbox": None, "time_push_production": None, } - for line in release.body.splitlines(): + release_body = release.body or "" + for line in release_body.splitlines(): m = regex_compiled_prefix.match(line) if m: if not m.group("remaining"): diff --git a/cumulusci/tasks/github/tests/test_release_report.py b/cumulusci/tasks/github/tests/test_release_report.py index 701b7ddf4f..de2f1eb1f1 100644 --- a/cumulusci/tasks/github/tests/test_release_report.py +++ b/cumulusci/tasks/github/tests/test_release_report.py @@ -49,6 +49,11 @@ def test_run_task(self): Sandbox orgs: 2018-08-01 Production orgs: 2018-09-01""", ), + self._get_expected_release( + "rel/2.1", + created_at="2018-01-02T00:00:00Z", + url="https://api.github.com/repos/SalesforceFoundation/Cumulus/releases/5", + ), self._get_expected_release( "rel/1.0", created_at="2017-01-01T00:00:00Z", @@ -91,5 +96,14 @@ def test_run_task(self): ), "time_push_sandbox": datetime(2018, 8, 1, 0, 0, 0, 2, tzinfo=pytz.UTC), "url": "", - } + }, + { + "beta": False, + "name": "1.0", + "tag": "rel/2.1", + "time_created": datetime(2018, 1, 2, 0, 0, tzinfo=pytz.UTC), + "time_push_production": None, + "time_push_sandbox": None, + "url": "", + }, ] == task.return_values["releases"] diff --git a/cumulusci/tasks/github/tests/util_github_api.py b/cumulusci/tasks/github/tests/util_github_api.py index 7dfeafb476..c807e0cd22 100644 --- a/cumulusci/tasks/github/tests/util_github_api.py +++ b/cumulusci/tasks/github/tests/util_github_api.py @@ -530,7 +530,7 @@ def _get_expected_release(self, tag_name, **kw): "assets": [], "assets_url": "", "author": self._get_expected_user("author"), - "body": "", + "body": None, "created_at": now, "draft": False, "html_url": "", From dabc98f4016c13f619a0c55c4d9582bf23477070 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Wed, 20 Sep 2023 12:23:58 +0530 Subject: [PATCH 25/98] CleanUp --- cumulusci/core/config/scratch_org_config.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cumulusci/core/config/scratch_org_config.py b/cumulusci/core/config/scratch_org_config.py index 17a845fe63..2e4eab967f 100644 --- a/cumulusci/core/config/scratch_org_config.py +++ b/cumulusci/core/config/scratch_org_config.py @@ -23,6 +23,7 @@ class ScratchOrgConfig(SfdxOrgConfig): instance: str password_failed: bool devhub: str + release: str createable: bool = True @@ -38,10 +39,6 @@ def scratch_info(self): def days(self) -> int: return self.config.setdefault("days", 1) - @property - def release(self) -> str: - return self.config.setdefault("release", None) - @property def active(self) -> bool: """Check if an org is alive""" From 8ffe5382dff55a5894290765df044d192577e816 Mon Sep 17 00:00:00 2001 From: David Reed Date: Wed, 20 Sep 2023 11:04:26 -0600 Subject: [PATCH 26/98] Query with install_key in promote_package_version (via @zenibako) (#3654) --- AUTHORS.rst | 1 + cumulusci/cumulusci.yml | 1 + cumulusci/tasks/create_package_version.py | 18 +++++--- .../salesforce/promote_package_version.py | 11 ++++- .../tests/test_create_package_version.py | 6 ++- .../tests/test_promote_package_version.py | 44 ++++++++++++++----- cumulusci/utils/salesforce/soql.py | 17 +++++++ cumulusci/utils/salesforce/tests/test_soql.py | 30 +++++++++++++ 8 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 cumulusci/utils/salesforce/soql.py create mode 100644 cumulusci/utils/salesforce/tests/test_soql.py diff --git a/AUTHORS.rst b/AUTHORS.rst index ba6dd22338..6801fd7b6d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -36,4 +36,5 @@ For example: * Ed Rivas (jerivas) * Gustavo Tandeciarz (dcinzona) +* Chandler Anderson (zenibako) * Ben French (BenjaminFrench) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 57cd1e0220..ab6adc8a55 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -1327,6 +1327,7 @@ flows: task: promote_package_version options: version_id: ^^create_package_version.subscriber_package_version_id + install_key: ^^create_package_version.install_key unmanaged_ee: group: Deployment description: Deploy the unmanaged package metadata and all dependencies to the target EE org diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index c9a08ca72e..ac958df95b 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -37,6 +37,9 @@ from cumulusci.tasks.salesforce.BaseSalesforceApiTask import BaseSalesforceApiTask from cumulusci.tasks.salesforce.org_settings import build_settings_package from cumulusci.utils.git import split_repo_url +from cumulusci.utils.salesforce.soql import ( + format_subscriber_package_version_where_clause, +) class PackageTypeEnum(StrEnum): @@ -251,9 +254,12 @@ def _run_task(self): ).format() # get the new version's dependencies from SubscriberPackageVersion + where_clause = format_subscriber_package_version_where_clause( + package2_version["SubscriberPackageVersionId"], + self.options.get("install_key"), + ) res = self.tooling.query( - "SELECT Dependencies FROM SubscriberPackageVersion " - f"WHERE Id='{package2_version['SubscriberPackageVersionId']}'" + "SELECT Dependencies FROM SubscriberPackageVersion " f"WHERE {where_clause}" ) self.return_values["dependencies"] = self._prepare_cci_dependencies( res["records"][0]["Dependencies"] @@ -338,7 +344,6 @@ def _create_version_request( version_bytes = io.BytesIO() version_info = zipfile.ZipFile(version_bytes, "w", zipfile.ZIP_DEFLATED) try: - # Add the package.zip package_hash = package_zip_builder.as_hash() version_info.writestr("package.zip", package_zip_builder.as_bytes()) @@ -441,8 +446,11 @@ def _create_version_request( "VersionInfo": version_info, "CalculateCodeCoverage": not skip_validation, } - if "install_key" in self.options: - request["InstallKey"] = self.options["install_key"] + + install_key = self.options.get("install_key") + if install_key: + request["InstallKey"] = install_key + self.return_values["install_key"] = install_key self.logger.info( f"Requesting creation of package version {version_number.format()} " diff --git a/cumulusci/tasks/salesforce/promote_package_version.py b/cumulusci/tasks/salesforce/promote_package_version.py index f040f27927..f17ca195af 100644 --- a/cumulusci/tasks/salesforce/promote_package_version.py +++ b/cumulusci/tasks/salesforce/promote_package_version.py @@ -9,6 +9,9 @@ from cumulusci.salesforce_api.utils import get_simple_salesforce_connection from cumulusci.tasks.create_package_version import PackageVersionNumber from cumulusci.tasks.salesforce import BaseSalesforceApiTask +from cumulusci.utils.salesforce.soql import ( + format_subscriber_package_version_where_clause, +) class PromotePackageVersion(BaseSalesforceApiTask): @@ -39,6 +42,9 @@ class PromotePackageVersion(BaseSalesforceApiTask): ), "required": False, }, + "install_key": { + "description": "Install key for package. Default is no install key." + }, } # We do use a Salesforce org, but it's the dev hub obtained using get_devhub_config, @@ -294,6 +300,7 @@ def _query_SubscriberPackage(self, sp_id: str) -> Optional[Dict]: def _query_SubscriberPackageVersion(self, spv_id: str) -> Optional[Dict]: """Queries for a SubscriberPackageVersion record with the given SubscriberPackageVersionId""" + return self._query_one_tooling( [ "Id", @@ -302,7 +309,9 @@ def _query_SubscriberPackageVersion(self, spv_id: str) -> Optional[Dict]: "SubscriberPackageId", ], "SubscriberPackageVersion", - where_clause=f"Id='{spv_id}'", + where_clause=format_subscriber_package_version_where_clause( + spv_id, self.options.get("install_key") + ), raise_error=True, ) diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index a870c4c19d..df901a35cb 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -274,7 +274,8 @@ def test_run_task( ], }, ) - responses.add( # query dependency org for installed package 1) + # query dependency org for installed package 1 + responses.add( "GET", f"{self.scratch_base_url}/tooling/query/", json={ @@ -290,7 +291,7 @@ def test_run_task( } ], }, - ), + ) responses.add( # query dependency org for installed package 2) "GET", f"{self.scratch_base_url}/tooling/query/", @@ -459,6 +460,7 @@ def test_run_task( assert task.return_values["dependencies"] == [ {"version_id": "04t000000000009AAA"} ] + assert task.return_values["install_key"] == task.options["install_key"] zf.close() @responses.activate diff --git a/cumulusci/tasks/tests/test_promote_package_version.py b/cumulusci/tasks/tests/test_promote_package_version.py index 71ef95d120..c6fe1014bc 100644 --- a/cumulusci/tasks/tests/test_promote_package_version.py +++ b/cumulusci/tasks/tests/test_promote_package_version.py @@ -1,4 +1,5 @@ import logging +from typing import Optional from unittest import mock import pytest @@ -62,22 +63,24 @@ class TestPromotePackageVersion(GithubApiTestMixin): f"https://devhub.my.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}" ) - def _mock_target_package_api_calls(self): + def _mock_target_package_api_calls(self, install_key: Optional[str] = None): + record = { + "BuildNumber": 0, + "Id": "main_package", + "IsReleased": False, + "MajorVersion": 1, + "MinorVersion": 0, + "PatchVersion": 0, + } + if install_key: + record["InstallKey"] = install_key + responses.add( # query for main package's Package2Version info "GET", f"{self.devhub_base_url}/tooling/query/", json={ "size": 1, - "records": [ - { - "BuildNumber": 0, - "Id": "main_package", - "IsReleased": False, - "MajorVersion": 1, - "MinorVersion": 0, - "PatchVersion": 0, - } - ], + "records": [record], "done": True, }, ) @@ -188,6 +191,25 @@ def test_run_task(self, task, devhub_config): ): task() + @responses.activate + def test_run_task__install_key(self, task, devhub_config): + # 20 dependencies, 10 are 2GP, 5 of those are not yet promoted + task.options["install_key"] = "hunter2" + self._mock_dependencies(20, 10, 5) + self._mock_target_package_api_calls(install_key="hunter2") + with mock.patch( + "cumulusci.tasks.salesforce.promote_package_version.get_devhub_config", + return_value=devhub_config, + ): + with mock.patch( + "cumulusci.tasks.salesforce.promote_package_version.get_simple_salesforce_connection", + ) as get_connection: + task() + assert ( + "hunter2" + in get_connection.return_value.query_all.call_args_list[0][0][0] + ) + @responses.activate def test_run_task__no_dependencies(self, task, devhub_config): self._mock_dependencies(0, 0, 0) diff --git a/cumulusci/utils/salesforce/soql.py b/cumulusci/utils/salesforce/soql.py new file mode 100644 index 0000000000..1152ebedbc --- /dev/null +++ b/cumulusci/utils/salesforce/soql.py @@ -0,0 +1,17 @@ +from typing import Optional + + +def format_subscriber_package_version_where_clause( + spv_id: str, install_key: Optional[str] = None +) -> str: + """Get the where clause for a SubscriberPackageVersion query + + Does not include the WHERE. + Includes the installation key if provided. + """ + where_clause = f"Id='{spv_id}'" + + if install_key: + where_clause += f" AND InstallationKey ='{install_key}'" + + return where_clause diff --git a/cumulusci/utils/salesforce/tests/test_soql.py b/cumulusci/utils/salesforce/tests/test_soql.py new file mode 100644 index 0000000000..a68419f602 --- /dev/null +++ b/cumulusci/utils/salesforce/tests/test_soql.py @@ -0,0 +1,30 @@ +import pytest + +from cumulusci.utils.salesforce.soql import ( + format_subscriber_package_version_where_clause, +) + + +class TestSoql: + spv_id = "04t000000000000" + + @pytest.mark.vcr() + def test_format_subscriber_package_version_where_clause_simple(self): + where_clause = format_subscriber_package_version_where_clause(self.spv_id, None) + assert f"Id='{self.spv_id}'" in where_clause + assert " AND InstallationKey =" not in where_clause + + @pytest.mark.vcr() + def format_subscriber_package_version_where_clause_install_key_set(self): + install_key = "hunter2" + where_clause = format_subscriber_package_version_where_clause( + self.spv_id, install_key + ) + assert f"Id='{self.spv_id}'" in where_clause + assert f" AND InstallationKey ='{install_key}'" in where_clause + + @pytest.mark.vcr() + def format_subscriber_package_version_where_clause_install_key_none(self): + where_clause = format_subscriber_package_version_where_clause(self.spv_id, None) + assert f"Id='{self.spv_id}'" in where_clause + assert " AND InstallationKey =" not in where_clause From 41883e293bc71edb8db0d0e01de38f6cff499af5 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Thu, 21 Sep 2023 12:49:59 +0530 Subject: [PATCH 27/98] Added Pydantic Validator to cumulusci.yml --- cumulusci/core/keychain/base_project_keychain.py | 4 +++- cumulusci/schema/cumulusci.jsonschema.json | 5 +++++ cumulusci/utils/yaml/cumulusci_yml.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cumulusci/core/keychain/base_project_keychain.py b/cumulusci/core/keychain/base_project_keychain.py index 92b2a1278d..561caeaec7 100644 --- a/cumulusci/core/keychain/base_project_keychain.py +++ b/cumulusci/core/keychain/base_project_keychain.py @@ -67,11 +67,13 @@ def create_scratch_org( else: # Use scratch config days or default of 1 day scratch_config.setdefault("days", 1) + if release is not None: + scratch_config["release"] = release scratch_config["set_password"] = bool(set_password) scratch_config["scratch"] = True scratch_config.setdefault("namespaced", False) scratch_config["config_name"] = config_name - scratch_config["release"] = release + scratch_config[ "sfdx_alias" ] = f"{self.project_config.project__name}__{org_name}" diff --git a/cumulusci/schema/cumulusci.jsonschema.json b/cumulusci/schema/cumulusci.jsonschema.json index 7d53f2c257..255d2de4d4 100644 --- a/cumulusci/schema/cumulusci.jsonschema.json +++ b/cumulusci/schema/cumulusci.jsonschema.json @@ -430,6 +430,11 @@ "noancestors": { "title": "Noancestors", "type": "boolean" + }, + "release": { + "title": "Release", + "enum": ["preview", "previous"], + "type": "string" } }, "additionalProperties": false diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index a85cee77bd..f8498ed9ea 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -150,6 +150,7 @@ class ScratchOrg(CCIDictModel): namespaced: str = None setup_flow: str = None noancestors: bool = None + release: Literal["preview", "previous"] = None class Orgs(CCIDictModel): From c0fe9eef9ca4955b21253efd149a111dbd8d22be Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Thu, 21 Sep 2023 20:42:09 +0530 Subject: [PATCH 28/98] Added Tests for remaining files --- cumulusci/core/config/tests/test_config_expensive.py | 2 ++ cumulusci/core/keychain/tests/test_base_project_keychain.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cumulusci/core/config/tests/test_config_expensive.py b/cumulusci/core/config/tests/test_config_expensive.py index c1ea451b06..c83a9b17d7 100644 --- a/cumulusci/core/config/tests/test_config_expensive.py +++ b/cumulusci/core/config/tests/test_config_expensive.py @@ -770,6 +770,7 @@ def test_build_org_create_args(self, scratch_def_file): "sfdx_alias": "project__org", "default": True, "instance": "NA01", + "release": "previous", }, "test", mock_keychain, @@ -786,6 +787,7 @@ def test_build_org_create_args(self, scratch_def_file): "--noancestors", "--durationdays", "1", + "release=previous", "-a", "project__org", "adminEmail=test@example.com", diff --git a/cumulusci/core/keychain/tests/test_base_project_keychain.py b/cumulusci/core/keychain/tests/test_base_project_keychain.py index 06e9757fdc..89cc12318a 100644 --- a/cumulusci/core/keychain/tests/test_base_project_keychain.py +++ b/cumulusci/core/keychain/tests/test_base_project_keychain.py @@ -152,9 +152,10 @@ def test_create_scratch_org(self, key): ) keychain = BaseProjectKeychain(project_config, key) keychain.key = key - keychain.create_scratch_org("test", "dev", days=3) + keychain.create_scratch_org("test", "dev", days=3, release="previous") org_config = keychain.get_org("test").config assert org_config["days"] == 3 + assert org_config["release"] == "previous" def test_load_scratch_orgs(self, keychain): assert list(keychain.orgs) == [] From ac810e57da516a687a30efba50ea0c060290054a Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:05:50 +0530 Subject: [PATCH 29/98] Modified service to trim whitespaces (#3661) --- cumulusci/cli/service.py | 6 ++-- cumulusci/cli/tests/test_service.py | 53 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/cumulusci/cli/service.py b/cumulusci/cli/service.py index f9a6208c4d..e85280f2e3 100644 --- a/cumulusci/cli/service.py +++ b/cumulusci/cli/service.py @@ -204,7 +204,7 @@ def callback(*args, **kwargs): set_global_default = kwargs.pop("default", False) serv_conf = dict( - (k, v) for k, v in list(kwargs.items()) if v is not None + (k, v.strip()) for k, v in list(kwargs.items()) if v is not None ) # remove None values # A service can define a callable to validate the service config @@ -305,7 +305,7 @@ def service_update( for attr in attributes: attr_name, attr_value = attr if attr_name in service_config.config: - service_config.config[attr_name] = attr_value + service_config.config[attr_name] = attr_value.strip() attributes_were_updated = True else: available_attributes = ", ".join(service_attributes) @@ -324,7 +324,7 @@ def service_update( ) if user_input != "": attributes_were_updated = True - service_config.config[attr] = user_input + service_config.config[attr] = user_input.strip() if attributes_were_updated: runtime.keychain.set_service(service_type, service_name, service_config) diff --git a/cumulusci/cli/tests/test_service.py b/cumulusci/cli/tests/test_service.py index 774f11f532..b8d1502f1f 100644 --- a/cumulusci/cli/tests/test_service.py +++ b/cumulusci/cli/tests/test_service.py @@ -295,6 +295,28 @@ def test_service_connect_validator_failure(): run_cli_command("service", "connect", "test", "test-alias", runtime=runtime) +def test_service_connect_without_whitespaces(): + attr_value = " Sample attr value " + attr_value_without_whitespaces = "Sample attr value" + runtime = BaseCumulusCI( + config={"services": {"test": {"attributes": {"attr": {"required": False}}}}} + ) + + result = run_cli_command( + "service", + "connect", + "test", + "test-alias", + "--attr", + attr_value, + runtime=runtime, + ) + + result = runtime.keychain.get_service("test", "test-alias") + assert attr_value != result.attr + assert attr_value_without_whitespaces == result.attr + + def test_service_update__success(): # Create a new color-picker service type runtime = CliRuntime( @@ -334,6 +356,37 @@ def test_service_update__success(): assert original_color not in result.output +def test_service_update_without_whitespaces(): + # Create a new color-picker service type + runtime = CliRuntime( + config={ + "services": {"color-picker": {"attributes": {"color": {"required": False}}}} + } + ) + # Setup an existing service of type color-picker + original_color = "Turquoise" + runtime.keychain.set_service( + "color-picker", + "foo", + ServiceConfig({"color": original_color}), + ) + # Update the existing service + chosen_color = " Maroon " + chosen_color_without_whitespaces = "Maroon" + result = run_cli_command( + "service", + "update", + "color-picker", + "foo", + input=f"{chosen_color}\n", + runtime=runtime, + ) + # ensure info was written without whitespaces + result = runtime.keychain.get_service("color-picker", "foo") + assert chosen_color != result.color + assert chosen_color_without_whitespaces == result.color + + def test_service_update_headless__success(): # Create a new color-picker service type runtime = CliRuntime( From a8da2c33f6bf124f05e24ac5c938b830f1bd81b5 Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Thu, 21 Sep 2023 21:54:23 +0530 Subject: [PATCH 30/98] Added Namespace in org info (#3662) Co-authored-by: David Reed --- cumulusci/cli/org.py | 1 + cumulusci/cli/tests/test_org.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 4dc5ed3e6f..c8ee718e6f 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -330,6 +330,7 @@ def org_info(runtime, org_name, print_json): "instance_url", "instance_name", "is_sandbox", + "namespace", "namespaced", "org_id", "org_type", diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index 01692e22a8..007e82249d 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -540,6 +540,7 @@ def test_org_info(self): "default": True, "password": None, "connected_app": "built-in", + "namespace": "test", } org_config.expires = date.today() org_config.latest_api_version = "42.0" @@ -557,6 +558,7 @@ def test_org_info(self): ["\x1b[1mconnected_app\x1b[0m", "built-in"], ["\x1b[1mdays\x1b[0m", "1"], ["\x1b[1mdefault\x1b[0m", "True"], + ["\x1b[1mnamespace\x1b[0m", "test"], ["\x1b[1mpassword\x1b[0m", "None"], ], ) From ebf3596831dd47e76c9f08c513d8744b18d0d2e4 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Thu, 21 Sep 2023 23:29:13 +0530 Subject: [PATCH 31/98] Extended 'Deploy' task to support REST API deployment (#3650) --- cumulusci/salesforce_api/rest_deploy.py | 148 ++++++++++ .../salesforce_api/tests/test_rest_deploy.py | 261 ++++++++++++++++++ cumulusci/tasks/salesforce/Deploy.py | 9 + .../tasks/salesforce/tests/test_Deploy.py | 52 +++- 4 files changed, 458 insertions(+), 12 deletions(-) create mode 100644 cumulusci/salesforce_api/rest_deploy.py create mode 100644 cumulusci/salesforce_api/tests/test_rest_deploy.py diff --git a/cumulusci/salesforce_api/rest_deploy.py b/cumulusci/salesforce_api/rest_deploy.py new file mode 100644 index 0000000000..70d532569a --- /dev/null +++ b/cumulusci/salesforce_api/rest_deploy.py @@ -0,0 +1,148 @@ +import base64 +import io +import json +import os +import time +import uuid +import zipfile +from typing import List, Union + +import requests + +PARENT_DIR_NAME = "metadata" + + +class RestDeploy: + def __init__( + self, + task, + package_zip: str, + purge_on_delete: Union[bool, str, None], + check_only: bool, + test_level: Union[str, None], + run_tests: List[str], + ): + # Initialize instance variables and configuration options + self.api_version = task.project_config.project__package__api_version + self.task = task + assert package_zip, "Package zip should not be None" + if purge_on_delete is None: + purge_on_delete = True + self._set_purge_on_delete(purge_on_delete) + self.check_only = "true" if check_only else "false" + self.test_level = test_level + self.package_zip = package_zip + self.run_tests = run_tests or [] + + def __call__(self): + self._boundary = str(uuid.uuid4()) + url = f"{self.task.org_config.instance_url}/services/data/v{self.api_version}/metadata/deployRequest" + headers = { + "Authorization": f"Bearer {self.task.org_config.access_token}", + "Content-Type": f"multipart/form-data; boundary={self._boundary}", + } + + # Prepare deployment options as JSON payload + deploy_options = { + "deployOptions": { + "allowMissingFiles": False, + "autoUpdatePackage": False, + "checkOnly": self.check_only, + "ignoreWarnings": False, + "performRetrieve": False, + "purgeOnDelete": self.purge_on_delete, + "rollbackOnError": False, + "runTests": self.run_tests, + "singlePackage": False, + "testLevel": self.test_level, + } + } + json_payload = json.dumps(deploy_options) + + # Construct the multipart/form-data request body + body = ( + f"--{self._boundary}\r\n" + f'Content-Disposition: form-data; name="json"\r\n' + f"Content-Type: application/json\r\n\r\n" + f"{json_payload}\r\n" + f"--{self._boundary}\r\n" + f'Content-Disposition: form-data; name="file"; filename="metadata.zip"\r\n' + f"Content-Type: application/zip\r\n\r\n" + ).encode("utf-8") + body += self._reformat_zip(self.package_zip) + body += f"\r\n--{self._boundary}--\r\n".encode("utf-8") + + response = requests.post(url, headers=headers, data=body) + response_json = response.json() + + if response.status_code == 201: + self.task.logger.info("Deployment request successful") + deploy_request_id = response_json["id"] + self._monitor_deploy_status(deploy_request_id) + else: + self.task.logger.error( + f"Deployment request failed with status code {response.status_code}" + ) + + # Set the purge_on_delete attribute based on org type + def _set_purge_on_delete(self, purge_on_delete): + if not purge_on_delete or purge_on_delete == "false": + self.purge_on_delete = "false" + else: + self.purge_on_delete = "true" + # Disable purge on delete entirely for non sandbox or DE orgs as it is + # not allowed + org_type = self.task.org_config.org_type + is_sandbox = self.task.org_config.is_sandbox + if org_type != "Developer Edition" and not is_sandbox: + self.purge_on_delete = "false" + + # Monitor the deployment status and log progress + def _monitor_deploy_status(self, deploy_request_id): + url = f"{self.task.org_config.instance_url}/services/data/v{self.api_version}/metadata/deployRequest/{deploy_request_id}?includeDetails=true" + headers = {"Authorization": f"Bearer {self.task.org_config.access_token}"} + + while True: + response = requests.get(url, headers=headers) + response_json = response.json() + self.task.logger.info( + f"Deployment {response_json['deployResult']['status']}" + ) + + if response_json["deployResult"]["status"] not in ["InProgress", "Pending"]: + # Handle the case when status has Failed + if response_json["deployResult"]["status"] == "Failed": + for failure in response_json["deployResult"]["details"][ + "componentFailures" + ]: + self.task.logger.error(self._construct_error_message(failure)) + return + time.sleep(5) + + # Reformat the package zip file to include parent directory + def _reformat_zip(self, package_zip): + zip_bytes = base64.b64decode(package_zip) + zip_stream = io.BytesIO(zip_bytes) + new_zip_stream = io.BytesIO() + + with zipfile.ZipFile(zip_stream, "r") as zip_ref: + with zipfile.ZipFile(new_zip_stream, "w") as new_zip_ref: + for item in zip_ref.infolist(): + # Choice of name for parent directory is irrelevant to functioning + new_item_name = os.path.join(PARENT_DIR_NAME, item.filename) + file_content = zip_ref.read(item.filename) + new_zip_ref.writestr(new_item_name, file_content) + + new_zip_bytes = new_zip_stream.getvalue() + return new_zip_bytes + + # Construct an error message from deployment failure details + def _construct_error_message(self, failure): + error_message = f"{str.upper(failure['problemType'])} in file {failure['fileName'][len(PARENT_DIR_NAME)+len('/'):]}: {failure['problem']}" + + if failure["lineNumber"] and failure["columnNumber"]: + error_message += ( + f" at line {failure['lineNumber']}:{failure['columnNumber']}" + ) + + return error_message diff --git a/cumulusci/salesforce_api/tests/test_rest_deploy.py b/cumulusci/salesforce_api/tests/test_rest_deploy.py new file mode 100644 index 0000000000..6a3794e35a --- /dev/null +++ b/cumulusci/salesforce_api/tests/test_rest_deploy.py @@ -0,0 +1,261 @@ +import base64 +import io +import unittest +import zipfile +from unittest.mock import MagicMock, Mock, call, patch + +from cumulusci.salesforce_api.rest_deploy import RestDeploy + + +def generate_sample_zip_data(parent=""): + # Create a sample ZIP with two files + zip_data = io.BytesIO() + with zipfile.ZipFile(zip_data, "w") as zip_file: + zip_file.writestr( + f"{parent}objects/mockfile1.obj", "Sample content for mockfile1" + ) + zip_file.writestr( + f"{parent}objects/mockfile2.obj", "Sample content for mockfile2" + ) + return base64.b64encode(zip_data.getvalue()).decode("utf-8") + + +class TestRestDeploy(unittest.TestCase): + # Setup method executed before each test method + def setUp(self): + self.mock_logger = Mock() + self.mock_task = MagicMock() + self.mock_task.logger = self.mock_logger + self.mock_task.org_config.instance_url = "https://example.com" + self.mock_task.org_config.access_token = "dummy_token" + self.mock_task.project_config.project__package__api_version = 58.0 + # Empty zip file for testing + self.mock_zip = generate_sample_zip_data() + + # Test case for a successful deployment and deploy status + @patch("requests.post") + @patch("requests.get") + def test_deployment_success(self, mock_get, mock_post): + + response_post = Mock(status_code=201) + response_post.json.return_value = {"id": "dummy_id"} + mock_post.return_value = response_post + + response_get = Mock(status_code=200) + response_get.json.side_effect = [ + {"deployResult": {"status": "InProgress"}}, + {"deployResult": {"status": "Succeeded"}}, + ] + mock_get.return_value = response_get + + deployer = RestDeploy( + self.mock_task, self.mock_zip, False, False, "NoTestRun", [] + ) + deployer() + + # Assertions to verify log messages + assert ( + call("Deployment request successful") + in self.mock_logger.info.call_args_list + ) + assert call("Deployment InProgress") in self.mock_logger.info.call_args_list + assert call("Deployment Succeeded") in self.mock_logger.info.call_args_list + + # Assertions to verify API Calls + expected_get_calls = [ + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + ] + + mock_post.assert_called_once() + mock_get.assert_has_calls(expected_get_calls, any_order=True) + + # Test case for a deployment failure + @patch("requests.post") + def test_deployment_failure(self, mock_post): + + response_post = Mock(status_code=500) + response_post.json.return_value = {"id": "dummy_id"} + mock_post.return_value = response_post + + deployer = RestDeploy( + self.mock_task, self.mock_zip, False, False, "NoTestRun", [] + ) + deployer() + + # Assertions to verify log messages + assert ( + call("Deployment request failed with status code 500") + in self.mock_logger.error.call_args_list + ) + + # Assertions to verify API Calls + mock_post.assert_called_once() + + # Test for deployment success but deploy status failure + @patch("requests.post") + @patch("requests.get") + def test_deployStatus_failure(self, mock_get, mock_post): + + response_post = Mock(status_code=201) + response_post.json.return_value = {"id": "dummy_id"} + mock_post.return_value = response_post + + response_get = Mock(status_code=200) + response_get.json.side_effect = [ + {"deployResult": {"status": "InProgress"}}, + { + "deployResult": { + "status": "Failed", + "details": { + "componentFailures": [ + { + "problemType": "Error", + "fileName": "metadata/classes/mockfile1.cls", + "problem": "someproblem1", + "lineNumber": 1, + "columnNumber": 1, + }, + { + "problemType": "Error", + "fileName": "metadata/objects/mockfile2.obj", + "problem": "someproblem2", + "lineNumber": 2, + "columnNumber": 2, + }, + ] + }, + } + }, + ] + mock_get.return_value = response_get + + deployer = RestDeploy( + self.mock_task, self.mock_zip, False, False, "NoTestRun", [] + ) + deployer() + + # Assertions to verify log messages + assert ( + call("Deployment request successful") + in self.mock_logger.info.call_args_list + ) + assert call("Deployment InProgress") in self.mock_logger.info.call_args_list + assert call("Deployment Failed") in self.mock_logger.info.call_args_list + assert ( + call("ERROR in file classes/mockfile1.cls: someproblem1 at line 1:1") + in self.mock_logger.error.call_args_list + ) + assert ( + call("ERROR in file objects/mockfile2.obj: someproblem2 at line 2:2") + in self.mock_logger.error.call_args_list + ) + + # Assertions to verify API Calls + expected_get_calls = [ + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + ] + + mock_post.assert_called_once() + mock_get.assert_has_calls(expected_get_calls, any_order=True) + + # Test case for a deployment with a pending status + @patch("requests.post") + @patch("requests.get") + def test_pending_call(self, mock_get, mock_post): + + response_post = Mock(status_code=201) + response_post.json.return_value = {"id": "dummy_id"} + mock_post.return_value = response_post + + response_get = Mock(status_code=200) + response_get.json.side_effect = [ + {"deployResult": {"status": "InProgress"}}, + {"deployResult": {"status": "Pending"}}, + {"deployResult": {"status": "Succeeded"}}, + ] + mock_get.return_value = response_get + + deployer = RestDeploy( + self.mock_task, self.mock_zip, False, False, "NoTestRun", [] + ) + deployer() + + # Assertions to verify log messages + assert ( + call("Deployment request successful") + in self.mock_logger.info.call_args_list + ) + assert call("Deployment InProgress") in self.mock_logger.info.call_args_list + assert call("Deployment Pending") in self.mock_logger.info.call_args_list + assert call("Deployment Succeeded") in self.mock_logger.info.call_args_list + + # Assertions to verify API Calls + expected_get_calls = [ + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + call( + "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + headers={"Authorization": "Bearer dummy_token"}, + ), + ] + + mock_post.assert_called_once() + mock_get.assert_has_calls(expected_get_calls, any_order=True) + + def test_reformat_zip(self): + input_zip = generate_sample_zip_data() + expected_zip = generate_sample_zip_data("metadata/") + + deployer = RestDeploy( + self.mock_task, self.mock_zip, False, False, "NoTestRun", [] + ) + actual_output_zip = deployer._reformat_zip(input_zip) + + self.assertEqual( + base64.b64encode(actual_output_zip).decode("utf-8"), expected_zip + ) + + def test_purge_on_delete(self): + test_data = [ + ("not_sandbox_developer", "Not Developer Edition", False, False, "false"), + ("purgeOnDelete_true", "Developer Edition", True, True, "true"), + ("purgeOnDelete_none", "Developer Edition", True, None, "true"), + ] + + for name, org_type, is_sandbox, purge_on_delete, expected_result in test_data: + with self.subTest(name=name): + self.mock_task.org_config.org_type = org_type + self.mock_task.org_config.is_sandbox = is_sandbox + deployer = RestDeploy( + self.mock_task, + self.mock_zip, + purge_on_delete, + False, + "NoTestRun", + [], + ) + self.assertEqual(deployer.purge_on_delete, expected_result) + + +if __name__ == "__main__": + unittest.main() diff --git a/cumulusci/tasks/salesforce/Deploy.py b/cumulusci/tasks/salesforce/Deploy.py index f84ae3ae24..4f42a8a14d 100644 --- a/cumulusci/tasks/salesforce/Deploy.py +++ b/cumulusci/tasks/salesforce/Deploy.py @@ -13,6 +13,7 @@ from cumulusci.core.utils import process_bool_arg, process_list_arg from cumulusci.salesforce_api.metadata import ApiDeploy from cumulusci.salesforce_api.package_zip import MetadataPackageZipBuilder +from cumulusci.salesforce_api.rest_deploy import RestDeploy from cumulusci.tasks.salesforce.BaseSalesforceMetadataApiTask import ( BaseSalesforceMetadataApiTask, ) @@ -55,6 +56,7 @@ class Deploy(BaseSalesforceMetadataApiTask): "transforms": { "description": "Apply source transforms before deploying. See the CumulusCI documentation for details on how to specify transforms." }, + "rest_deploy": {"description": "If True, deploy metadata using REST API"}, } namespaces = {"sf": "http://soap.sforce.com/2006/04/metadata"} @@ -99,6 +101,9 @@ def _init_options(self, kwargs): f"The validation error was {str(e)}" ) + # Set class variable to true if rest_deploy is set to True + self.rest_deploy = process_bool_arg(self.options.get("rest_deploy", False)) + def _get_api(self, path=None): if not path: path = self.options.get("path") @@ -110,6 +115,10 @@ def _get_api(self, path=None): self.logger.warning("Deployment package is empty; skipping deployment.") return + # If rest_deploy param is set, update api_class to be RestDeploy + if self.rest_deploy: + self.api_class = RestDeploy + return self.api_class( self, package_zip, diff --git a/cumulusci/tasks/salesforce/tests/test_Deploy.py b/cumulusci/tasks/salesforce/tests/test_Deploy.py index a08211f117..d971f3a230 100644 --- a/cumulusci/tasks/salesforce/tests/test_Deploy.py +++ b/cumulusci/tasks/salesforce/tests/test_Deploy.py @@ -15,7 +15,8 @@ class TestDeploy: - def test_get_api(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api(self, rest_deploy): with temporary_dir() as path: touch("package.xml") task = create_task( @@ -26,6 +27,7 @@ def test_get_api(self): "namespace_inject": "ns", "namespace_strip": "ns", "unmanaged": True, + "rest_deply": rest_deploy, }, ) @@ -34,11 +36,18 @@ def test_get_api(self): assert "package.xml" in zf.namelist() zf.close() - def test_get_api__managed(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api__managed(self, rest_deploy): with temporary_dir() as path: touch("package.xml") task = create_task( - Deploy, {"path": path, "namespace_inject": "ns", "unmanaged": False} + Deploy, + { + "path": path, + "namespace_inject": "ns", + "unmanaged": False, + "rest_deploy": rest_deploy, + }, ) api = task._get_api() @@ -46,7 +55,8 @@ def test_get_api__managed(self): assert "package.xml" in zf.namelist() zf.close() - def test_get_api__additional_options(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api__additional_options(self, rest_deploy): with temporary_dir() as path: touch("package.xml") task = create_task( @@ -56,6 +66,7 @@ def test_get_api__additional_options(self): "test_level": "RunSpecifiedTests", "specified_tests": "TestA,TestB", "unmanaged": False, + "rest_deploy": rest_deploy, }, ) @@ -63,7 +74,8 @@ def test_get_api__additional_options(self): assert api.run_tests == ["TestA", "TestB"] assert api.test_level == "RunSpecifiedTests" - def test_get_api__skip_clean_meta_xml(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api__skip_clean_meta_xml(self, rest_deploy): with temporary_dir() as path: touch("package.xml") task = create_task( @@ -72,6 +84,7 @@ def test_get_api__skip_clean_meta_xml(self): "path": path, "clean_meta_xml": False, "unmanaged": True, + "rest_deploy": rest_deploy, }, ) @@ -80,7 +93,8 @@ def test_get_api__skip_clean_meta_xml(self): assert "package.xml" in zf.namelist() zf.close() - def test_get_api__static_resources(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api__static_resources(self, rest_deploy): with temporary_dir() as path: with open("package.xml", "w") as f: f.write( @@ -107,6 +121,7 @@ def test_get_api__static_resources(self): "namespace_inject": "ns", "namespace_strip": "ns", "unmanaged": True, + "rest_deploy": rest_deploy, }, ) @@ -120,32 +135,37 @@ def test_get_api__static_resources(self): assert "TestBundle" in package_xml zf.close() - def test_get_api__missing_path(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api__missing_path(self, rest_deploy): task = create_task( Deploy, { "path": "BOGUS", "unmanaged": True, + "rest_deploy": rest_deploy, }, ) api = task._get_api() assert api is None - def test_get_api__empty_package_zip(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_get_api__empty_package_zip(self, rest_deploy): with temporary_dir() as path: task = create_task( Deploy, { "path": path, "unmanaged": True, + "rest_deploy": rest_deploy, }, ) api = task._get_api() assert api is None - def test_init_options(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_init_options(self, rest_deploy): with pytest.raises(TaskOptionsError): create_task( Deploy, @@ -153,6 +173,7 @@ def test_init_options(self): "path": "empty", "test_level": "RunSpecifiedTests", "unmanaged": False, + "rest_deploy": rest_deploy, }, ) @@ -169,34 +190,40 @@ def test_init_options(self): "test_level": "RunLocalTests", "specified_tests": ["TestA"], "unmanaged": False, + "rest_deploy": rest_deploy, }, ) - def test_init_options__transforms(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_init_options__transforms(self, rest_deploy): d = create_task( Deploy, { "path": "src", "transforms": ["clean_meta_xml"], + "rest_deploy": rest_deploy, }, ) assert len(d.transforms) == 1 assert isinstance(d.transforms[0], CleanMetaXMLTransform) - def test_init_options__bad_transforms(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_init_options__bad_transforms(self, rest_deploy): with pytest.raises(TaskOptionsError) as e: create_task( Deploy, { "path": "src", "transforms": [{}], + "rest_deploy": rest_deploy, }, ) assert "transform spec is not valid" in str(e) - def test_freeze_sets_kind(self): + @pytest.mark.parametrize("rest_deploy", [True, False]) + def test_freeze_sets_kind(self, rest_deploy): task = create_task( Deploy, { @@ -204,6 +231,7 @@ def test_freeze_sets_kind(self): "namespace_tokenize": "ns", "namespace_inject": "ns", "namespace_strip": "ns", + "rest_deploy": rest_deploy, }, ) step = StepSpec( From 34229c6cd2ac37104b4c89eae785910a16933f91 Mon Sep 17 00:00:00 2001 From: Jawadtp <142204917+mjawadtp@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:14:14 +0530 Subject: [PATCH 32/98] W-12214520: Adding ApexTestSuite support in run_tests (#3660) --- cumulusci/tasks/apex/testrunner.py | 89 ++++++++++++++++- cumulusci/tasks/apex/tests/test_apex_tasks.py | 99 ++++++++++++++++++- 2 files changed, 180 insertions(+), 8 deletions(-) diff --git a/cumulusci/tasks/apex/testrunner.py b/cumulusci/tasks/apex/testrunner.py index 5accf25144..3ba6ef1783 100644 --- a/cumulusci/tasks/apex/testrunner.py +++ b/cumulusci/tasks/apex/testrunner.py @@ -120,7 +120,6 @@ class RunApexTests(BaseSalesforceApiTask): "project__test__name_match from project config. " "Comma-separated list for multiple patterns." ), - "required": True, }, "test_name_exclude": { "description": ( @@ -171,6 +170,9 @@ class RunApexTests(BaseSalesforceApiTask): "description": "By default, only failures get detailed output. " "Set verbose to True to see all passed test methods." }, + "test_suite_names": { + "description": "Accepts a comma-separated list of test suite names. Only runs test classes that are part of the test suites specified." + }, } def _init_options(self, kwargs): @@ -179,11 +181,16 @@ def _init_options(self, kwargs): self.options["test_name_match"] = self.options.get( "test_name_match", self.project_config.project__test__name_match ) - self.options["test_name_exclude"] = self.options.get( "test_name_exclude", self.project_config.project__test__name_exclude ) + self.options["test_suite_names"] = self.options.get( + "test_suite_names", self.project_config.project__test__suite__names + ) + if self.options["test_name_match"] is None: + self.options["test_name_match"] = "" + if self.options["test_name_exclude"] is None: self.options["test_name_exclude"] = "" @@ -236,6 +243,14 @@ def _init_options(self, kwargs): self.required_per_class_code_coverage_percent = int( self.options.get("required_per_class_code_coverage_percent", 0) ) + # Raises a TaskOptionsError when the user provides both test_suite_names and test_name_match. + if (self.options["test_suite_names"]) and ( + self.options["test_name_match"] is not None + and self.options["test_name_match"] != "%_TEST%" + ): + raise TaskOptionsError( + "Both test_suite_names and test_name_match cannot be passed simultaneously" + ) # pylint: disable=W0201 def _init_class(self): @@ -283,13 +298,78 @@ def _get_test_class_query(self): return query def _get_test_classes(self): + # If test_suite_names is provided, execute only tests that are a part of the list of test suites provided. + if self.options["test_suite_names"]: + test_classes_from_test_suite_names = ( + self._get_test_classes_from_test_suite_names() + ) + return test_classes_from_test_suite_names + + # test_suite_names is not provided. Fetch all the test classes from the org. + else: + return self._get_all_test_classes() + + def _get_all_test_classes(self): + # Fetches all the test classes from the org. query = self._get_test_class_query() - # Run the query - self.logger.info("Running query: {}".format(query)) + self.logger.info("Fetching all the test classes...") result = self.tooling.query_all(query) self.logger.info("Found {} test classes".format(result["totalSize"])) return result + def _get_comma_separated_string_of_items(self, itemlist): + # Accepts a list of strings. A formatted string is returned. + # Example: Input: ['TestSuite1', 'TestSuite2'] Output: ''TestSuite1','TestSuite2'' + return ",".join([f"'{item}'" for item in itemlist]) + + def _get_test_suite_ids_from_test_suite_names_query(self, test_suite_names_arg): + # Returns a query string which when executed fetches the test suite ids of the list of test suite names. + test_suite_names = self._get_comma_separated_string_of_items( + test_suite_names_arg.split(",") + ) + query1 = f"SELECT Id, TestSuiteName FROM ApexTestSuite WHERE TestSuiteName IN ({test_suite_names})" + return query1 + + def _get_test_classes_from_test_suite_ids_query(self, testSuiteIds): + # Returns a query string which when executed fetches Apex test classes for the given list of test suite ids. + # Apex test classes passed under test_name_exclude are ignored. + testSuiteIds_formatted = self._get_comma_separated_string_of_items(testSuiteIds) + + if len(testSuiteIds_formatted) == 0: + testSuiteIds_formatted = "''" + + test_name_exclude_arg = self.options["test_name_exclude"] + condition = "" + + # Check if test_name_exclude is provided. Append to query string if the former is specified. + if test_name_exclude_arg: + test_name_exclude = self._get_comma_separated_string_of_items( + test_name_exclude_arg.split(",") + ) + condition = f"AND Name NOT IN ({test_name_exclude})" + + query = f"SELECT Id, Name FROM ApexClass WHERE Id IN (SELECT ApexClassId FROM TestSuiteMembership WHERE ApexTestSuiteId IN ({testSuiteIds_formatted})) {condition}" + return query + + def _get_test_classes_from_test_suite_names(self): + # Returns a list of Apex test classes that belong to the test suite(s) specified. Test classes specified in test_name_exclude are excluded. + test_suite_names_arg = self.options["test_suite_names"] + query1 = self._get_test_suite_ids_from_test_suite_names_query( + test_suite_names_arg + ) + self.logger.info("Fetching test suite metadata...") + result = self.tooling.query_all(query1) + testSuiteIds = [] + + for record in result["records"]: + testSuiteIds.append(str(record["Id"])) + + query2 = self._get_test_classes_from_test_suite_ids_query(testSuiteIds) + self.logger.info("Fetching test classes belonging to the test suite(s)...") + result = self.tooling.query_all(query2) + self.logger.info("Found {} test classes".format(result["totalSize"])) + return result + def _get_test_methods_for_class(self, class_name): result = self.tooling.query( f"SELECT SymbolTable FROM ApexClass WHERE Name='{class_name}'" @@ -515,7 +595,6 @@ def _init_task(self): ) def _run_task(self): - result = self._get_test_classes() if result["totalSize"] == 0: return diff --git a/cumulusci/tasks/apex/tests/test_apex_tasks.py b/cumulusci/tasks/apex/tests/test_apex_tasks.py index 8f7f654844..ad2a3651ef 100644 --- a/cumulusci/tasks/apex/tests/test_apex_tasks.py +++ b/cumulusci/tasks/apex/tests/test_apex_tasks.py @@ -152,7 +152,7 @@ def _get_mock_test_query_results(self, methodnames, outcomes, messages): return_value = {"done": True, "records": []} - for (method_name, outcome, message) in zip(methodnames, outcomes, messages): + for method_name, outcome, message in zip(methodnames, outcomes, messages): this_result = deepcopy(record_base) this_result["Message"] = message this_result["Outcome"] = outcome @@ -726,6 +726,101 @@ def test_init_options__bad_regexes(self): task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) + def test_get_test_suite_ids_from_test_suite_names_query__multiple_test_suites(self): + # Test to ensure that query to fetch test suite ids from test suite names is formed properly when multiple test suites are specified. + task_config = TaskConfig( + { + "options": { + "test_suite_names": "TestSuite1,TestSuite2", + "test_name_match": "%_TEST%", + } + } + ) + task = RunApexTests(self.project_config, task_config, self.org_config) + test_suite_names_arg = "TestSuite1,TestSuite2" + query = task._get_test_suite_ids_from_test_suite_names_query( + test_suite_names_arg + ) + + assert ( + "SELECT Id, TestSuiteName FROM ApexTestSuite WHERE TestSuiteName IN ('TestSuite1','TestSuite2')" + == query + ) + + def test_get_test_suite_ids_from_test_suite_names_query__single_test_suite(self): + # Test to ensure that query to fetch test suite ids from test suite names is formed properly when a single test suite is specified. + + task_config = TaskConfig( + { + "options": { + "test_suite_names": "TestSuite1", + "test_name_match": "%_TEST%", + } + } + ) + test_suite_names_arg = "TestSuite1" + task = RunApexTests(self.project_config, task_config, self.org_config) + query = task._get_test_suite_ids_from_test_suite_names_query( + test_suite_names_arg + ) + + assert ( + "SELECT Id, TestSuiteName FROM ApexTestSuite WHERE TestSuiteName IN ('TestSuite1')" + == query + ) + + def test_get_test_classes_from_test_suite_ids_query__no_test_name_exclude(self): + # Test to ensure that query to fetch test classes from test suite ids is formed properly when no test_name_exclude is specified. + task_config = TaskConfig() + task = RunApexTests(self.project_config, task_config, self.org_config) + test_suite_ids = ["id1", "id2"] + query = task._get_test_classes_from_test_suite_ids_query(test_suite_ids) + assert ( + "SELECT Id, Name FROM ApexClass WHERE Id IN (SELECT ApexClassId FROM TestSuiteMembership WHERE ApexTestSuiteId IN ('id1','id2')) " + == query + ) + + def test_get_test_classes_from_test_suite_ids_query__with_test_name_exclude(self): + # Test to ensure that query to fetch test classes from test suite ids is formed properly when test_name_exclude is specified. + task_config = TaskConfig({"options": {"test_name_exclude": "Test1,Test2"}}) + task = RunApexTests(self.project_config, task_config, self.org_config) + test_suite_ids = ["id1", "id2"] + query = task._get_test_classes_from_test_suite_ids_query(test_suite_ids) + assert ( + "SELECT Id, Name FROM ApexClass WHERE Id IN (SELECT ApexClassId FROM TestSuiteMembership WHERE ApexTestSuiteId IN ('id1','id2')) AND Name NOT IN ('Test1','Test2')" + == query + ) + + def test_get_comma_separated_string_of_items__multiple_items(self): + # Test to ensure that a comma separated string of items is properly formed when a list of strings with multiple strings is passed. + task_config = TaskConfig() + task = RunApexTests(self.project_config, task_config, self.org_config) + itemlist = ["TestSuite1", "TestSuite2"] + item_string = task._get_comma_separated_string_of_items(itemlist) + assert item_string == "'TestSuite1','TestSuite2'" + + def test_get_comma_separated_string_of_items__single_item(self): + # Test to ensure that a comma separated string of items is properly formed when a list of strings with a single string is passed. + task_config = TaskConfig() + task = RunApexTests(self.project_config, task_config, self.org_config) + itemlist = ["TestSuite1"] + item_string = task._get_comma_separated_string_of_items(itemlist) + assert item_string == "'TestSuite1'" + + def test_init_options__test_suite_names_and_test_name_match_provided(self): + # Test to ensure that a TaskOptionsError is raised when both test_suite_names and test_name_match are provided. + task_config = TaskConfig( + { + "options": { + "test_name_match": "sample", + "test_suite_names": "suite1,suite2", + } + } + ) + with pytest.raises(TaskOptionsError): + task = RunApexTests(self.project_config, task_config, self.org_config) + task._init_options(task_config.config["options"]) + def test_get_namespace_filter__managed(self): task_config = TaskConfig({"options": {"managed": True, "namespace": "testns"}}) task = RunApexTests(self.project_config, task_config, self.org_config) @@ -1266,7 +1361,6 @@ def mock_poll_action(): @responses.activate def test_job_not_found(self): - task, url = self._get_url_and_task() response = self._get_query_resp() response["records"] = [] @@ -1301,7 +1395,6 @@ def test_run_tests__integration_test(self, create_task, caplog, vcr): self._test_run_tests__integration_test(create_task, caplog) def _test_run_tests__integration_test(self, create_task, caplog): - caplog.set_level(logging.INFO) with pytest.raises(exc.ApexTestException) as e: task = create_task( From dda6937ad24a9f50ecb8ca28b420a343378326cc Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Fri, 29 Sep 2023 02:22:23 +0530 Subject: [PATCH 33/98] Implemented variable substitution for nested structures (#3665) Co-authored-by: Josh Kofsky --- cumulusci/core/tasks.py | 23 ++++++++++++++++++----- cumulusci/core/tests/test_tasks.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cumulusci/core/tasks.py b/cumulusci/core/tasks.py index c2e95c9db9..e9f5b2f115 100644 --- a/cumulusci/core/tasks.py +++ b/cumulusci/core/tasks.py @@ -135,13 +135,26 @@ def _init_options(self, kwargs): self.options.update(kwargs) # Handle dynamic lookup of project_config values via $project_config.attr - for option, value in self.options.items(): - if isinstance(value, str): - value = PROJECT_CONFIG_RE.sub( + def process_options(option): + if isinstance(option, str): + return PROJECT_CONFIG_RE.sub( lambda match: str(self.project_config.lookup(match.group(1), None)), - value, + option, ) - self.options[option] = value + elif isinstance(option, dict): + processed_dict = {} + for key, value in option.items(): + processed_dict[key] = process_options(value) + return processed_dict + elif isinstance(option, list): + processed_list = [] + for item in option: + processed_list.append(process_options(item)) + return processed_list + else: + return option + + self.options = process_options(self.options) if self.Options: try: diff --git a/cumulusci/core/tests/test_tasks.py b/cumulusci/core/tests/test_tasks.py index 579c1bfb0a..38535a8c46 100644 --- a/cumulusci/core/tests/test_tasks.py +++ b/cumulusci/core/tests/test_tasks.py @@ -105,6 +105,22 @@ def test_init_options__project_config_substitution(self): task = BaseTask(self.project_config, self.task_config, self.org_config) assert task.options["test_option"] == "baz" + # For variable substitution in nested structures + def test_init_options__project_config_substitution_nested(self): + self.project_config.config["foo"] = {"bar": "baz", "fighters": "pretender"} + self.project_config.config["vulf"] = {"peck": "DeanTown"} + self.task_config.config["options"] = { + "test_option": "$project_config.foo__bar", + "songs": [ + {"foo_fighters": "$project_config.foo__fighters"}, + {"vulfpeck": "$project_config.vulf__peck"}, + ], + } + task = BaseTask(self.project_config, self.task_config, self.org_config) + assert task.options["test_option"] == "baz" + assert task.options["songs"][0]["foo_fighters"] == "pretender" + assert task.options["songs"][1]["vulfpeck"] == "DeanTown" + def test_init_options__not_shared(self): self.project_config.config["foo"] = {"bar": "baz"} self.task_config.config["options"] = {} From 2aeceef1d4c5b05fc5344faa8f07426db71bdf26 Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Fri, 29 Sep 2023 02:49:10 +0530 Subject: [PATCH 34/98] Exceptions Handled (#3656) --- cumulusci/tasks/salesforce/package_upload.py | 21 ++++++- .../salesforce/tests/test_PackageUpload.py | 55 ++++++++++++------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/cumulusci/tasks/salesforce/package_upload.py b/cumulusci/tasks/salesforce/package_upload.py index f58029ee68..15c6392938 100644 --- a/cumulusci/tasks/salesforce/package_upload.py +++ b/cumulusci/tasks/salesforce/package_upload.py @@ -89,7 +89,8 @@ def _validate_versions(self): ORDER BY MajorVersion DESC, MinorVersion DESC, - PatchVersion DESC + PatchVersion DESC, + ReleaseState DESC LIMIT 1 """ ), @@ -111,7 +112,23 @@ def _validate_versions(self): # Updates minor version when not passed in remaining cases. if self.options["major_version"] == str(version["MajorVersion"]): if "minor_version" in self.options: - if int(self.options["minor_version"]) <= version["MinorVersion"]: + try: + if int(self.options["minor_version"]) < version["MinorVersion"]: + raise TaskOptionsError("Minor Version not valid.") + elif ( + int(self.options["minor_version"]) == version["MinorVersion"] + and version["ReleaseState"] == "Released" + ): + raise TaskOptionsError("Minor Version not valid.") + else: + if ( + int(self.options["minor_version"]) > version["MinorVersion"] + and version["ReleaseState"] == "Beta" + ): + raise TaskOptionsError( + "Latest Minor Version is Beta so minor version cannot be greater than that." + ) + except ValueError: raise TaskOptionsError("Minor Version not valid.") else: if version["ReleaseState"] == "Beta": diff --git a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py index c98a2e7970..cceac1e68f 100644 --- a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py +++ b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py @@ -178,26 +178,20 @@ def test_positive_validate_versions(self, actual_options, expected_options): assert task.options["major_version"] == expected_options["major_version"] assert task.options["minor_version"] == expected_options["minor_version"] - def test_positive_validate_versions_for_beta(self): - actual_options = { - "name": "Test Release", - "production": False, - "description": "Test Description", - "password": "secret", - "post_install_url": "post.install.url", - "release_notes_url": "release.notes.url", - "major_version": "1", - } - expected_options = { - "name": "Test Release", - "production": False, - "description": "Test Description", - "password": "secret", - "post_install_url": "post.install.url", - "release_notes_url": "release.notes.url", - "major_version": "1", - "minor_version": "1", - } + test_positive_options_beta = [ + generate_valid_version_options("1", None, "1", "1"), + generate_valid_version_options("1", "1", "1", "1"), + generate_valid_version_options(None, "1", "1", "1"), + generate_valid_version_options(None, None, "1", "1"), + ] + + @pytest.mark.parametrize( + "actual_options,expected_options", test_positive_options_beta + ) + def test_positive_validate_versions_for_beta( + self, actual_options, expected_options + ): + task = create_task(PackageUpload, actual_options) task._get_one_record = mock.Mock( return_value={ @@ -225,6 +219,7 @@ def test_positive_validate_versions_for_beta(self): generate_valid_version_options("1", "0", None, None, True), generate_valid_version_options(None, "1", None, None, True), generate_valid_version_options("ab", 0, None, None, True), + generate_valid_version_options("1", "ab", None, None, True), ] @pytest.mark.parametrize("actual_options", test_negative_options) @@ -242,6 +237,26 @@ def test_negative_validate_versions(self, actual_options): with pytest.raises(TaskOptionsError): task._validate_versions() + test_negative_options_beta = [ + generate_valid_version_options("1", "2", None, None, True), + generate_valid_version_options(None, "2", None, None, True), + ] + + @pytest.mark.parametrize("actual_options", test_negative_options_beta) + def test_negative_validate_versions_beta(self, actual_options): + """Running Negative Tests for tests_validate_versions""" + task = create_task(PackageUpload, actual_options) + task._get_one_record = mock.Mock( + return_value={ + "MajorVersion": 1, + "MinorVersion": 1, + "PatchVersion": 0, + "ReleaseState": "Beta", + } + ) + with pytest.raises(TaskOptionsError): + task._validate_versions() + def test_set_package_info(self): expected_package_id = "12345" options = { From 43435c348656ebb13ff8e797a11e3d24f5d2ee22 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Fri, 29 Sep 2023 03:27:51 +0530 Subject: [PATCH 35/98] Added xpath 'find_and_replace' functionality to transforms (#3655) --- .../tests/test_transforms.py | 721 ++++++++++++------ .../core/source_transforms/transforms.py | 86 ++- docs/deploy.md | 78 +- 3 files changed, 637 insertions(+), 248 deletions(-) diff --git a/cumulusci/core/source_transforms/tests/test_transforms.py b/cumulusci/core/source_transforms/tests/test_transforms.py index 4fb9015d5d..99230f2fe2 100644 --- a/cumulusci/core/source_transforms/tests/test_transforms.py +++ b/cumulusci/core/source_transforms/tests/test_transforms.py @@ -7,6 +7,7 @@ from zipfile import ZipFile import pytest +from lxml import etree as ET from pydantic import ValidationError from cumulusci.core.exceptions import CumulusCIException, TaskOptionsError @@ -376,304 +377,546 @@ def test_bundle_static_resources(task_context): assert compare_spec == zf -def test_find_replace_static(task_context): +def create_builder(task_context, zip_content, patterns): builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("Foo.cls"): "System.debug('blah');", - } - ).as_zipfile(), + ZipFileSpec(zip_content).as_zipfile(), context=task_context, transforms=[ FindReplaceTransform( - FindReplaceTransformOptions.parse_obj( - {"patterns": [{"find": "bl", "replace": "ye"}]} - ) + FindReplaceTransformOptions.parse_obj({"patterns": patterns}) ) ], ) - assert ( - ZipFileSpec( - { - Path("Foo.cls"): "System.debug('yeah');", - } - ) - == builder.zf - ) + return builder + + +def zip_assert(builder, modified_zip_content): + assert ZipFileSpec(modified_zip_content) == builder.zf + + +def test_find_replace_static(task_context): + zip_content = { + Path("Foo.cls"): "System.debug('blah');", + } + patterns = [{"find": "bl", "replace": "ye"}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path("Foo.cls"): "System.debug('yeah');", + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_static(task_context): + zip_content = { + Path( + "Foo.xml" + ): "blahblah", + } + patterns = [{"xpath": "/root/element1", "replace": "yeah"}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): "yeahblah", + } + zip_assert(builder, modified_zip_content) + + +def test_find_replace_xmlFile(task_context): + zip_content = { + Path( + "Foo.xml" + ): "blahblah", + Path("Bar.cls"): "System.debug('blah');", + } + patterns = [{"find": "bl", "replace": "ye"}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): "yeahyeah", + Path("Bar.cls"): "System.debug('yeah');", + } + zip_assert(builder, modified_zip_content) def test_find_replace_environ(task_context): with mock.patch.dict(os.environ, {"INSERT_TEXT": "ye"}): - builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("Foo.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=task_context, - transforms=[ - FindReplaceTransform( - FindReplaceTransformOptions.parse_obj( - {"patterns": [{"find": "bl", "replace_env": "INSERT_TEXT"}]} - ) - ) - ], - ) + zip_content = { + Path("Foo.cls"): "System.debug('blah');", + } + patterns = [{"find": "bl", "replace_env": "INSERT_TEXT"}] + builder = create_builder(task_context, zip_content, patterns) - assert ( - ZipFileSpec( - { - Path("Foo.cls"): "System.debug('yeah');", - } - ) - == builder.zf - ) + modified_zip_content = { + Path("Foo.cls"): "System.debug('yeah');", + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_environ(task_context): + with mock.patch.dict(os.environ, {"INSERT_TEXT": "yeah"}): + zip_content = { + Path( + "Foo.xml" + ): "blahblah", + } + patterns = [{"xpath": "/root/element1", "replace_env": "INSERT_TEXT"}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): "yeahblah", + } + zip_assert(builder, modified_zip_content) def test_find_replace_environ__not_found(task_context): assert "INSERT_TEXT" not in os.environ with pytest.raises(TaskOptionsError): - MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("Foo.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=task_context, - transforms=[ - FindReplaceTransform( - FindReplaceTransformOptions.parse_obj( - {"patterns": [{"find": "bl", "replace_env": "INSERT_TEXT"}]} - ) - ) - ], - ) + zip_content = { + Path("Foo.cls"): "System.debug('blah');", + } + patterns = [{"find": "bl", "replace_env": "INSERT_TEXT"}] + create_builder(task_context, zip_content, patterns) -def test_find_replace_filtered(task_context): - builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('blah');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=task_context, - transforms=[ - FindReplaceTransform( - FindReplaceTransformOptions.parse_obj( - { - "patterns": [ - {"find": "bl", "replace": "ye", "paths": ["classes"]} - ] - } - ) - ) - ], - ) +def test_xpath_replace_environ__not_found(task_context): + assert "INSERT_TEXT" not in os.environ + with pytest.raises(TaskOptionsError): + zip_content = { + Path( + "Foo.xml" + ): "blahblah", + } + patterns = [{"xpath": "/root/element1", "replace_env": "INSERT_TEXT"}] + create_builder(task_context, zip_content, patterns) - assert ( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('yeah');", - Path("Bar.cls"): "System.debug('blah');", - } - ) - == builder.zf - ) +def test_find_replace_filtered(task_context): + zip_content = { + Path("classes") / "Foo.cls": "System.debug('blah');", + Path("Bar.cls"): "System.debug('blah');", + } + patterns = [{"find": "bl", "replace": "ye", "paths": ["classes"]}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path("classes") / "Foo.cls": "System.debug('yeah');", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_filtered(task_context): + zip_content = { + Path("classes") + / "Foo.xml": "blahblah", + Path( + "Bar.cls" + ): "blahblah", + } + patterns = [{"xpath": "/root/element1", "replace": "yeah", "paths": ["classes"]}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path("classes") + / "Foo.xml": "yeahblah", + Path( + "Bar.cls" + ): "blahblah", + } + zip_assert(builder, modified_zip_content) -def test_find_replace_multiple(task_context): - builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('blah');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=task_context, - transforms=[ - FindReplaceTransform( - FindReplaceTransformOptions.parse_obj( - { - "patterns": [ - {"find": "bl", "replace": "ye", "paths": ["classes"]}, - {"find": "ye", "replace": "ha"}, - ] - } - ) - ) - ], - ) - assert ( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('haah');", - Path("Bar.cls"): "System.debug('blah');", - } - ) - == builder.zf - ) +def test_find_replace_multiple(task_context): + zip_content = { + Path("classes") / "Foo.cls": "System.debug('blah');", + Path("Bar.cls"): "System.debug('blah');", + } + patterns = [ + {"find": "bl", "replace": "ye", "paths": ["classes"]}, + {"find": "ye", "replace": "ha"}, + ] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path("classes") / "Foo.cls": "System.debug('haah');", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_multiple(task_context): + zip_content = { + Path("classes") + / "Foo.xml": "blahblah", + Path( + "Bar.cls" + ): "blahblah", + } + patterns = [ + {"xpath": "/root/element1", "replace": "yeah", "paths": ["classes"]}, + {"xpath": "/root/element1", "replace": "haah", "paths": ["classes"]}, + {"xpath": "/root/element3", "replace": "yeah", "paths": ["classes"]}, + ] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path("classes") + / "Foo.xml": "haahblah", + Path( + "Bar.cls" + ): "blahblah", + } + zip_assert(builder, modified_zip_content) def test_find_replace_current_user(task_context): - options = FindReplaceTransformOptions.parse_obj( - { - "patterns": [ - {"find": "%%%CURRENT_USER%%%", "inject_username": True}, - ] - } - ) - builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('%%%CURRENT_USER%%%');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=task_context, - transforms=[FindReplaceTransform(options)], - ) - + patterns = [ + {"find": "%%%CURRENT_USER%%%", "inject_username": True}, + ] + zip_content = { + Path("classes") / "Foo.cls": "System.debug('%%%CURRENT_USER%%%');", + Path("Bar.cls"): "System.debug('blah');", + } + builder = create_builder(task_context, zip_content, patterns) expected_username = task_context.org_config.username - assert ( - ZipFileSpec( - { - Path("classes") / "Foo.cls": f"System.debug('{expected_username}');", - Path("Bar.cls"): "System.debug('blah');", - } - ) - == builder.zf - ) + modified_zip_content = { + Path("classes") / "Foo.cls": f"System.debug('{expected_username}');", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_current_user(task_context): + patterns = [ + {"xpath": "/root/element1", "inject_username": True}, + ] + zip_content = { + Path("classes") + / "Foo.cls": "blahblah", + Path("Bar.cls"): "System.debug('blah');", + } + builder = create_builder(task_context, zip_content, patterns) + expected_username = task_context.org_config.username + modified_zip_content = { + Path("classes") + / "Foo.cls": f"{expected_username}blah", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) def test_find_replace_org_url(task_context): - options = FindReplaceTransformOptions.parse_obj( + patterns = [ { - "patterns": [ - { - "find": "{url}", - "inject_org_url": True, - }, - ] - } - ) - builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('{url}');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=task_context, - transforms=[FindReplaceTransform(options)], - ) + "find": "{url}", + "inject_org_url": True, + }, + ] + zip_content = { + Path("classes") / "Foo.cls": "System.debug('{url}');", + Path("Bar.cls"): "System.debug('blah');", + } + builder = create_builder(task_context, zip_content, patterns) + instance_url = task_context.org_config.instance_url + modified_zip_content = { + Path("classes") / "Foo.cls": f"System.debug('{instance_url}');", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) + +def test_xpath_replace_org_url(task_context): + patterns = [ + { + "xpath": "/root/element1", + "inject_org_url": True, + }, + ] + zip_content = { + Path("classes") + / "Foo.cls": "blahblah", + Path("Bar.cls"): "System.debug('blah');", + } + builder = create_builder(task_context, zip_content, patterns) instance_url = task_context.org_config.instance_url - assert ( - ZipFileSpec( - { - Path("classes") / "Foo.cls": f"System.debug('{instance_url}');", - Path("Bar.cls"): "System.debug('blah');", - } - ) - == builder.zf - ) + modified_zip_content = { + Path("classes") + / "Foo.cls": f"{instance_url}blah", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) @pytest.mark.parametrize("api", [FindReplaceIdAPI.REST, FindReplaceIdAPI.TOOLING]) -def test_find_replace_id(api): +def test_xpath_replace_id(api): context = mock.Mock() result = {"totalSize": 1, "records": [{"Id": "00D"}]} context.org_config.salesforce_client.query.return_value = result context.org_config.tooling.query.return_value = result - options = FindReplaceTransformOptions.parse_obj( + patterns = [ { - "patterns": [ - { - "find": "00Y", - "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", - "api": api, - }, - ] - } - ) - builder = MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('00Y');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=context, - transforms=[FindReplaceTransform(options)], - ) + "xpath": "/root/element1", + "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", + "api": api, + }, + ] + zip_content = { + Path("classes") + / "Foo.cls": "blahblah", + Path("Bar.cls"): "System.debug('blah');", + } + builder = create_builder(context, zip_content, patterns) + modified_zip_content = { + Path("classes") + / "Foo.cls": "00Dblah", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) - assert ( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('00D');", - Path("Bar.cls"): "System.debug('blah');", - } - ) - == builder.zf - ) + +@pytest.mark.parametrize("api", [FindReplaceIdAPI.REST, FindReplaceIdAPI.TOOLING]) +def test_find_replace_id(api): + context = mock.Mock() + result = {"totalSize": 1, "records": [{"Id": "00D"}]} + context.org_config.salesforce_client.query.return_value = result + context.org_config.tooling.query.return_value = result + patterns = [ + { + "find": "00Y", + "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", + "api": api, + }, + ] + zip_content = { + Path("classes") / "Foo.cls": "System.debug('00Y');", + Path("Bar.cls"): "System.debug('blah');", + } + builder = create_builder(context, zip_content, patterns) + modified_zip_content = { + Path("classes") / "Foo.cls": "System.debug('00D');", + Path("Bar.cls"): "System.debug('blah');", + } + zip_assert(builder, modified_zip_content) def test_find_replace_id__bad_query_result(): context = mock.Mock() result = {"totalSize": 0} context.org_config.salesforce_client.query.return_value = result - options = FindReplaceTransformOptions.parse_obj( + patterns = [ { - "patterns": [ - { - "find": "00Y", - "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", - }, - ] - } - ) + "find": "00Y", + "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", + }, + ] + zip_content = { + Path("classes") / "Foo.cls": "System.debug('00Y');", + Path("Bar.cls"): "System.debug('blah');", + } with pytest.raises(CumulusCIException): - MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('00Y');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=context, - transforms=[FindReplaceTransform(options)], - ) + create_builder(context, zip_content, patterns) + + +def test_xpath_replace_id__bad_query_result(): + context = mock.Mock() + result = {"totalSize": 0} + context.org_config.salesforce_client.query.return_value = result + patterns = [ + { + "xpath": "/root/element1", + "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", + }, + ] + zip_content = { + Path("classes") + / "Foo.cls": "blahblah", + Path("Bar.cls"): "System.debug('blah');", + } + with pytest.raises(CumulusCIException): + create_builder(context, zip_content, patterns) def test_find_replace_id__no_id_returned(): context = mock.Mock() result = {"totalSize": 1, "records": [{"name": "foo"}]} context.org_config.salesforce_client.query.return_value = result - options = FindReplaceTransformOptions.parse_obj( + patterns = [ { - "patterns": [ - { - "find": "00Y", - "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", - }, - ] - } - ) + "find": "00Y", + "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", + }, + ] + zip_content = { + Path("classes") / "Foo.cls": "System.debug('00Y');", + Path("Bar.cls"): "System.debug('blah');", + } with pytest.raises(CumulusCIException): - MetadataPackageZipBuilder.from_zipfile( - ZipFileSpec( - { - Path("classes") / "Foo.cls": "System.debug('00Y');", - Path("Bar.cls"): "System.debug('blah');", - } - ).as_zipfile(), - context=context, - transforms=[FindReplaceTransform(options)], - ) + create_builder(context, zip_content, patterns) + + +def test_xpath_replace_id__no_id_returned(): + context = mock.Mock() + result = {"totalSize": 1, "records": [{"name": "foo"}]} + context.org_config.salesforce_client.query.return_value = result + patterns = [ + { + "xpath": "/root/element1", + "replace_record_id_query": "SELECT Id FROM Account WHERE name='Initech Corp.'", + }, + ] + zip_content = { + Path("classes") + / "Foo.cls": "blahblah", + Path("Bar.cls"): "System.debug('blah');", + } + with pytest.raises(CumulusCIException): + create_builder(context, zip_content, patterns) + + +def test_find_xpath_both_none(task_context): + zip_content = { + Path("Foo.cls"): "System.debug('blah');", + } + patterns = [{"replace": "ye"}] + with pytest.raises(ValidationError) as e: + create_builder(task_context, zip_content, patterns) + assert "Input is not valid. Please pass either find or xpath paramter." in str(e) + + +def test_find_xpath_both_empty(task_context): + zip_content = { + Path("Foo.cls"): "System.debug('blah');", + } + patterns = [{"find": "", "xpath": "", "replace": "ye"}] + with pytest.raises(ValidationError) as e: + create_builder(task_context, zip_content, patterns) + assert "Input is not valid. Please pass either find or xpath paramter." in str(e) + + +def test_find_xpath_both(task_context): + zip_content = { + Path("Foo.cls"): "System.debug('blah');", + } + patterns = [{"find": "bl", "xpath": "bl", "replace": "ye"}] + with pytest.raises(ValidationError) as e: + create_builder(task_context, zip_content, patterns) + assert ( + "Input is not valid. Please pass either find or xpath paramter not both." + in str(e) + ) + + +def test_invalid_xpath(task_context): + zip_content = { + Path( + "Foo.xml" + ): "blahblah", + } + patterns = [{"xpath": '/root/element1[tezxt()=="blah"]', "replace": "yeah"}] + with pytest.raises(ET.XPathError): + create_builder(task_context, zip_content, patterns) + + +def test_xpath_replace_with_index(task_context): + zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + patterns = [{"xpath": "/bookstore/book[2]/title", "replace": "New Title"}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 New Title J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_with_text(task_context): + zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + patterns = [ + { + "xpath": "/bookstore/book/title[text()='Learning XML']", + "replace": "Updated Text", + } + ] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Updated Text Erik T. Ray 2003 39.95 ', + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_with_exp_and_index(task_context): + zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + patterns = [ + {"xpath": "/bookstore/book[price>40]/author[2]", "replace": "Rich Author"} + ] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Rich Author Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_with_exp_and_index2(task_context): + zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + patterns = [ + {"xpath": "/bookstore/book[price<40]/author[1]", "replace": "Rich Author"} + ] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Rich Author 2005 30.00 Harry Potter Rich Author 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Rich Author 2003 39.95 ', + } + zip_assert(builder, modified_zip_content) + + +def test_xpath_replace_with_exp(task_context): + zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start James McGovern Per Bothner Kurt Cagle James Linn Vaidyanathan Nagarajan 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + patterns = [{"xpath": "/bookstore/book[price>40]/author", "replace": "Rich Author"}] + builder = create_builder(task_context, zip_content, patterns) + + modified_zip_content = { + Path( + "Foo.xml" + ): ' Everyday Italian Giada De Laurentiis 2005 30.00 Harry Potter J K. Rowling 2005 29.99 XQuery Kick Start Rich Author Rich Author Rich Author Rich Author Rich Author 2003 49.99 Learning XML Erik T. Ray 2003 39.95 ', + } + zip_assert(builder, modified_zip_content) def test_source_transform_parsing(): diff --git a/cumulusci/core/source_transforms/transforms.py b/cumulusci/core/source_transforms/transforms.py index 146802bb50..ab311b1456 100644 --- a/cumulusci/core/source_transforms/transforms.py +++ b/cumulusci/core/source_transforms/transforms.py @@ -2,12 +2,14 @@ import functools import io import os +import re import shutil import typing as T import zipfile from pathlib import Path from zipfile import ZipFile +from lxml import etree as ET from pydantic import BaseModel, root_validator from cumulusci.core.dependencies.utils import TaskContext @@ -288,9 +290,29 @@ def process(self, zf: ZipFile, context: TaskContext) -> ZipFile: class FindReplaceBaseSpec(BaseModel, abc.ABC): - find: str + find: T.Optional[str] + xpath: T.Optional[str] paths: T.Optional[T.List[Path]] = None + @root_validator + def validate_find_xpath(cls, values): + findVal = values.get("find") + xpathVal = values.get("xpath") + if (findVal == "" or findVal is None) and (xpathVal is None or xpathVal == ""): + raise ValueError( + "Input is not valid. Please pass either find or xpath paramter." + ) + if ( + findVal != "" + and findVal is not None + and xpathVal != "" + and xpathVal is not None + ): + raise ValueError( + "Input is not valid. Please pass either find or xpath paramter not both." + ) + return values + @abc.abstractmethod def get_replace_string(self, context: TaskContext) -> str: ... @@ -394,15 +416,71 @@ def __init__(self, options: FindReplaceTransformOptions): self.options = options def process(self, zf: ZipFile, context: TaskContext) -> ZipFile: + # To handle xpath with namespaces, without + def transform_xpath(expression): + predicate_pattern = re.compile(r"\[.*?\]") + parts = expression.split("/") + transformed_parts = [] + + for part in parts: + if part: + predicates = predicate_pattern.findall(part) + tag = predicate_pattern.sub("", part) + transformed_part = '/*[local-name()="' + tag + '"]' + for predicate in predicates: + transformed_part += predicate + transformed_parts.append(transformed_part) + transformed_expression = "".join(transformed_parts) + + return transformed_expression + def process_file(filename: str, content: str) -> T.Tuple[str, str]: path = Path(filename) for spec in self.options.patterns: if not spec.paths or any( parent in path.parents for parent in spec.paths ): - content = content.replace( - spec.find, spec.get_replace_string(context) - ) + try: + # See if the content is an xml file + content_bytes = content.encode("utf-8") + root = ET.fromstring(content_bytes) + + # See if content has an xml declaration + has_xml_declaration = content.strip().startswith(" Date: Fri, 29 Sep 2023 07:04:40 +0530 Subject: [PATCH 36/98] Improve message description handling for scratch org cli `not_found` error (#3659) --------- Co-authored-by: David Reed Co-authored-by: Josh Kofsky --- cumulusci/core/config/scratch_org_config.py | 13 +++++++++++ .../config/tests/test_config_expensive.py | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/cumulusci/core/config/scratch_org_config.py b/cumulusci/core/config/scratch_org_config.py index 4f2b5766f2..06e81198d4 100644 --- a/cumulusci/core/config/scratch_org_config.py +++ b/cumulusci/core/config/scratch_org_config.py @@ -81,6 +81,18 @@ def create_org(self) -> None: def raise_error() -> NoReturn: message = f"{FAILED_TO_CREATE_SCRATCH_ORG}: \n{stdout}\n{stderr}" + try: + output = json.loads(stdout) + if ( + output.get("message") == "The requested resource does not exist" + and output.get("name") == "NOT_FOUND" + ): + raise ScratchOrgException( + "The Salesforce CLI was unable to create a scratch org. Ensure you are connected using a valid API version on an active Dev Hub." + ) + except json.decoder.JSONDecodeError: + raise ScratchOrgException(message) + raise ScratchOrgException(message) result = {} # for type checker. @@ -88,6 +100,7 @@ def raise_error() -> NoReturn: raise_error() try: result = json.loads(stdout) + except json.decoder.JSONDecodeError: raise_error() diff --git a/cumulusci/core/config/tests/test_config_expensive.py b/cumulusci/core/config/tests/test_config_expensive.py index c1ea451b06..13485a7b77 100644 --- a/cumulusci/core/config/tests/test_config_expensive.py +++ b/cumulusci/core/config/tests/test_config_expensive.py @@ -606,6 +606,29 @@ def test_create_org(self, Command): assert config.config["created"] assert config.scratch_org_type == "workspace" + def test_check_apiversion_error(self, Command): + out = b"""{ + "context": "Create", + "commandName": "Create", + "message": "The requested resource does not exist", + "name": "NOT_FOUND" + }""" + + Command.return_value = mock.Mock( + stdout=io.BytesIO(out), stderr=io.BytesIO(b""), returncode=1 + ) + config = ScratchOrgConfig( + {"config_file": "tmp.json", "email_address": "test@example.com"}, "test" + ) + with temporary_dir(): + with open("tmp.json", "w") as f: + f.write("{}") + with pytest.raises( + SfdxOrgException, + match="The Salesforce CLI was unable to create a scratch org. Ensure you are connected using a valid API version on an active Dev Hub.", + ): + config.create_org() + def test_create_org_no_config_file(self, Command): config = ScratchOrgConfig({}, "test") with pytest.raises(ScratchOrgException, match="missing a config_file"): From 7e2068ca51893037c88f1f9d0f1bdb7223e83ab2 Mon Sep 17 00:00:00 2001 From: lakshmi2506 <141401869+lakshmi2506@users.noreply.github.com> Date: Sat, 30 Sep 2023 00:42:01 +0530 Subject: [PATCH 37/98] Added deactivate_flows_task (#3664) Co-authored-by: David Reed Co-authored-by: Josh Kofsky --- cumulusci/cumulusci.yml | 9 +++ cumulusci/tasks/salesforce/activate_flow.py | 35 ++++++++---- .../salesforce/tests/test_activate_flow.py | 56 ++++++++++++++++--- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index ab6adc8a55..3714013e8a 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -5,6 +5,15 @@ tasks: group: Metadata Transformations description: Activates Flows identified by a given list of Developer Names class_path: cumulusci.tasks.salesforce.activate_flow.ActivateFlow + options: + status: True + + deactivate_flow: + group: Metadata Transformations + description: deactivates Flows identified by a given list of Developer Names + class_path: cumulusci.tasks.salesforce.activate_flow.ActivateFlow + options: + status: False add_page_layout_related_lists: group: Metadata Transformations description: Adds specified Related List to one or more Page Layouts. diff --git a/cumulusci/tasks/salesforce/activate_flow.py b/cumulusci/tasks/salesforce/activate_flow.py index 9e6a203f1d..9bbdbc2c64 100644 --- a/cumulusci/tasks/salesforce/activate_flow.py +++ b/cumulusci/tasks/salesforce/activate_flow.py @@ -12,7 +12,11 @@ class ActivateFlow(BaseSalesforceApiTask): "developer_names": { "description": "List of DeveloperNames to query in SOQL", "required": True, - } + }, + "status": { + "description": "Flag to check whether to activate or deactivate the flow", + "required": False, + }, } def _init_options(self, kwargs): @@ -20,35 +24,46 @@ def _init_options(self, kwargs): self.options["developer_names"] = process_list_arg( self.options.get("developer_names") ) - self.api_version = "43.0" + self.api_version = "58.0" if not self.options["developer_names"]: raise TaskOptionsError( "Error you are missing developer_names definition in your task cumulusci.yml file. Please pass in developer_names for your task configuration or use -o to developer_names as a commandline argument" ) def _run_task(self): - self.logger.info( - f"Activating the following Flows: {self.options['developer_names']}" - ) + if self.options["status"]: + self.logger.info( + f"Activating the following Flows: {self.options['developer_names']}" + ) + else: + self.logger.info( + f"Deactivating the following Flows: {self.options['developer_names']}" + ) + self.logger.info("Querying flow definitions...") result = self.tooling.query( "SELECT Id, ActiveVersion.VersionNumber, LatestVersion.VersionNumber, DeveloperName FROM FlowDefinition WHERE DeveloperName IN ({0})".format( ",".join([f"'{n}'" for n in self.options["developer_names"]]) ) ) + results = [] for listed_flow in result["records"]: results.append(listed_flow["DeveloperName"]) self.logger.info(f'Processing: {listed_flow["DeveloperName"]}') path = f"tooling/sobjects/FlowDefinition/{listed_flow['Id']}" + urlpath = self.sf.base_url + path - data = { - "Metadata": { - "activeVersionNumber": listed_flow["LatestVersion"]["VersionNumber"] - } - } + + if self.options["status"]: + updated_version_number = listed_flow["LatestVersion"]["VersionNumber"] + else: + updated_version_number = 0 + data = {"Metadata": {"activeVersionNumber": updated_version_number}} + self.logger.info(urlpath) response = self.tooling._call_salesforce("PATCH", urlpath, json=data) self.logger.info(response) + excluded = [] for i in self.options["developer_names"]: if i not in results: diff --git a/cumulusci/tasks/salesforce/tests/test_activate_flow.py b/cumulusci/tasks/salesforce/tests/test_activate_flow.py index 30c03d25ca..c89a7c2a2f 100644 --- a/cumulusci/tasks/salesforce/tests/test_activate_flow.py +++ b/cumulusci/tasks/salesforce/tests/test_activate_flow.py @@ -18,18 +18,19 @@ def test_activate_some_flow_processes(self): "developer_names": [ "Auto_Populate_Date_And_Name_On_Program_Engagement", "ape", - ] + ], + "status": True, }, ) record_id = "3001F0000009GFwQAM" activate_url = ( - "{}/services/data/v43.0/tooling/sobjects/FlowDefinition/{}".format( + "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( cc_task.org_config.instance_url, record_id ) ) responses.add( method="GET", - url="https://test.salesforce.com/services/data/v43.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", + url="https://test.salesforce.com/services/data/v58.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", body=json.dumps( { "records": [ @@ -49,6 +50,46 @@ def test_activate_some_flow_processes(self): cc_task() assert 2 == len(responses.calls) + @responses.activate + def test_deactivate_some_flow_processes(self): + cc_task = create_task( + ActivateFlow, + { + "developer_names": [ + "Auto_Populate_Date_And_Name_On_Program_Engagement", + "ape", + ], + "status": False, + }, + ) + record_id = "3001F0000009GFwQAM" + activate_url = ( + "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( + cc_task.org_config.instance_url, record_id + ) + ) + responses.add( + method="GET", + url="https://test.salesforce.com/services/data/v58.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", + body=json.dumps( + { + "records": [ + { + "Id": record_id, + "DeveloperName": "Auto_Populate_Date_And_Name_On_Program_Engagement", + "LatestVersion": {"VersionNumber": 1}, + } + ] + } + ), + status=200, + ) + data = {"Metadata": {"activeVersionNumber": 0}} + responses.add(method=responses.PATCH, url=activate_url, status=204, json=data) + + cc_task() + assert 2 == len(responses.calls) + @responses.activate def test_activate_all_flow_processes(self): cc_task = create_task( @@ -57,24 +98,25 @@ def test_activate_all_flow_processes(self): "developer_names": [ "Auto_Populate_Date_And_Name_On_Program_Engagement", "ape", - ] + ], + "status": True, }, ) record_id = "3001F0000009GFwQAM" record_id2 = "3001F0000009GFwQAW" activate_url = ( - "{}/services/data/v43.0/tooling/sobjects/FlowDefinition/{}".format( + "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( cc_task.org_config.instance_url, record_id ) ) activate_url2 = ( - "{}/services/data/v43.0/tooling/sobjects/FlowDefinition/{}".format( + "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( cc_task.org_config.instance_url, record_id2 ) ) responses.add( method="GET", - url="https://test.salesforce.com/services/data/v43.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", + url="https://test.salesforce.com/services/data/v58.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", body=json.dumps( { "records": [ From e2e5c4d329de485beecb7c82aa6c17b2d5664ddd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:15:07 +0000 Subject: [PATCH 38/98] Release v3.80.0 (#3666) --------- Co-authored-by: github-actions Co-authored-by: Josh Kofsky Co-authored-by: David Reed --- cumulusci/__about__.py | 2 +- docs/history.md | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 907f89c800..32fd96398d 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.79.0" +__version__ = "3.80.0" diff --git a/docs/history.md b/docs/history.md index 66b75c1644..9cfa1bf2cf 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,43 @@ +## v3.80.0 (2023-09-29) + + + +## What's Changed + +### Changes 🎉 + +- Allow setting Major and Minor Version in upload_production task by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3651](https://github.com/SFDO-Tooling/CumulusCI/pull/3651) +- Add better error handling for empty or invalid org and service env vars by [@prescod](https://github.com/prescod) in [#3365](https://github.com/SFDO-Tooling/CumulusCI/pull/3365) +- Query with install_key in promote_package_version (via @zenibako) by [@davidmreed](https://github.com/davidmreed) in [#3654](https://github.com/SFDO-Tooling/CumulusCI/pull/3654) +- Trim whitespaces during service data entry by [@aditya-balachander](https://github.com/aditya-balachander) in [#3661](https://github.com/SFDO-Tooling/CumulusCI/pull/3661) +- Display namespace in output of `cci org info` by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3662](https://github.com/SFDO-Tooling/CumulusCI/pull/3662) +- Extend `deploy` task to support REST API deployment by [@aditya-balachander](https://github.com/aditya-balachander) in [#3650](https://github.com/SFDO-Tooling/CumulusCI/pull/3650) +- Add `ApexTestSuite` support in the `run_tests` task by [@mjawadtp](https://github.com/mjawadtp) in [#3660](https://github.com/SFDO-Tooling/CumulusCI/pull/3660) +- Implement variable substitution for nested structures in cumulusci.yml by [@aditya-balachander](https://github.com/aditya-balachander) in [#3665](https://github.com/SFDO-Tooling/CumulusCI/pull/3665) +- Add XPath-based 'find_replace' functionality to `deploy` by [@aditya-balachander](https://github.com/aditya-balachander) in [#3655](https://github.com/SFDO-Tooling/CumulusCI/pull/3655) +- Improve message description handling for scratch org cli `not_found` error by [@lakshmi2506](https://github.com/lakshmi2506) in [#3659](https://github.com/SFDO-Tooling/CumulusCI/pull/3659) +- Add `deactivate_flows` task by [@lakshmi2506](https://github.com/lakshmi2506) in [#3664](https://github.com/SFDO-Tooling/CumulusCI/pull/3664) + +### Issues Fixed 🩴 + +- Add guard for empty body to github_release_report by [@jstvz](https://github.com/jstvz) in [#3645](https://github.com/SFDO-Tooling/CumulusCI/pull/3645) + +## New Contributors + +- @zenibako made their first contribution in [#3654](https://github.com/SFDO-Tooling/CumulusCI/pull/3654) + +* @jain-naman-sf made their first contribution in [#3651](https://github.com/SFDO-Tooling/CumulusCI/pull/3651) +* @aditya-balachander made their first contribution in [#3661](https://github.com/SFDO-Tooling/CumulusCI/pull/3661) +* @mjawadtp made their first contribution in [#3660](https://github.com/SFDO-Tooling/CumulusCI/pull/3660) +* @lakshmi2506 made their first contribution in [#3659](https://github.com/SFDO-Tooling/CumulusCI/pull/3659) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.79.0...v3.80.0 + + + ## v3.79.0 (2023-09-07) @@ -14,8 +51,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.78.0...v3.79.0 - - ## v3.78.0 (2023-08-10) @@ -4218,7 +4253,6 @@ the original set of flows was designed. > - **dependencies** Runs the pre-package deployment dependency > tasks **update_dependencies** and **deploy_pre** This flow is > called by almost all the main flows. - > > - **config\_**\* flows provide a place to customize the package > configuration for different environments. These flows are > called by the main flows after the package metadata is From 6a6082e1aea9d0432336b901e7edc9343e408514 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Wed, 4 Oct 2023 15:39:23 +0530 Subject: [PATCH 39/98] temp_changes --- cumulusci/cumulusci.yml | 10 +++++ cumulusci/salesforce_api/metadata.py | 39 ++++++++++++++++++ cumulusci/salesforce_api/soap_envelopes.py | 14 +++++++ cumulusci/tasks/metadata_type_list.py | 40 +++++++++++++++++++ .../tasks/salesforce/RetrieveMetadataTypes.py | 26 ++++++++++++ cumulusci/tasks/salesforce/__init__.py | 1 + 6 files changed, 130 insertions(+) create mode 100644 cumulusci/tasks/metadata_type_list.py create mode 100644 cumulusci/tasks/salesforce/RetrieveMetadataTypes.py diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 3714013e8a..5f8fb787ae 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -8,6 +8,10 @@ tasks: options: status: True + metadata_types: + class_path: cumulusci.tasks.metadata_type_list.MetadataTypeList + + deactivate_flow: group: Metadata Transformations description: deactivates Flows identified by a given list of Developer Names @@ -477,12 +481,18 @@ tasks: options: path: packaged group: Salesforce Metadata + + retrieve_metadatatypes: + class_path: cumulusci.tasks.salesforce.RetrieveMetadataTypes + group: Salesforce Metadatatypes retrieve_src: description: Retrieves the packaged metadata into the src directory class_path: cumulusci.tasks.salesforce.RetrievePackaged options: path: src group: Salesforce Metadata + + retrieve_unpackaged: description: Retrieve the contents of a package.xml file. class_path: cumulusci.tasks.salesforce.RetrieveUnpackaged diff --git a/cumulusci/salesforce_api/metadata.py b/cumulusci/salesforce_api/metadata.py index 9c13e56656..ad02aff976 100644 --- a/cumulusci/salesforce_api/metadata.py +++ b/cumulusci/salesforce_api/metadata.py @@ -678,3 +678,42 @@ def _process_response(self, response): ) # Unknown response raise MetadataApiError(f"Unexpected response: {response.text}", response) + + +class ApiListMetadataTypes(BaseMetadataApiCall): + check_interval = 1 + soap_envelope_start = soap_envelopes.METADATA_TYPES + soap_action_start = "describemetadatatypes" + + def __init__( + self, task, as_of_version=None + ): + super(ApiListMetadataTypes, self).__init__(task) + self.metadata_types = [] + self.as_of_version = ( + as_of_version + if as_of_version + else task.project_config.project__package__api_version + ) + self.api_version = self.as_of_version + + + + def _build_envelope_start(self): + + return self.soap_envelope_start.format( + as_of_version=self.as_of_version, + ) + + def _process_response(self, response): + self.metadata_types=[] + temp=parseString(response).getElementsByTagName("metadataObjects") + + for metadataobject in temp: + self.metadata_types.append(self._get_element_value(metadataobject, "xmlName")) + child_elements = metadataobject.getElementsByTagName("childXmlNames") + child_xml_names = [element.firstChild.nodeValue for element in child_elements] + self.metadata_types+=child_xml_names + + return self.metadata_types + diff --git a/cumulusci/salesforce_api/soap_envelopes.py b/cumulusci/salesforce_api/soap_envelopes.py index a685628342..3ccf02c0d0 100644 --- a/cumulusci/salesforce_api/soap_envelopes.py +++ b/cumulusci/salesforce_api/soap_envelopes.py @@ -161,3 +161,17 @@ """ + +METADATA_TYPES= """ + + + + ###SESSION_ID### + + + + + {as_of_version} + + +""" diff --git a/cumulusci/tasks/metadata_type_list.py b/cumulusci/tasks/metadata_type_list.py new file mode 100644 index 0000000000..83b0631bca --- /dev/null +++ b/cumulusci/tasks/metadata_type_list.py @@ -0,0 +1,40 @@ +import sarge +import json +from cumulusci.core.tasks import BaseTask +from cumulusci.core.sfdx import sfdx + +class MetadataTypeList(BaseTask): + + task_options = { + "org_username": { + "description": "Username for the org", + "required":True, + }, + } + + def _run_task(self): + + p: sarge.Command = sfdx( + f"force mdapi describemetadata --json ", + + username=self.options["org_username"], + ) + stdout = p.stdout_text.read() + + self.metadata_list=[] + + if p.returncode: + self.logger.error( f"Couldn't load list of Metadata types: \n{stdout}") + + else: + data=json.loads(stdout) + self.logger.info("List of Metadata types enabled for the org : ") + + for x in data['result']['metadataObjects']: + self.metadata_list.append(x['xmlName']) + self.metadata_list+=x['childXmlNames'] + + self.logger.info(self.metadata_list) + + + \ No newline at end of file diff --git a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py new file mode 100644 index 0000000000..96622b7316 --- /dev/null +++ b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py @@ -0,0 +1,26 @@ +from typing import Any +from cumulusci.salesforce_api.metadata import ApiListMetadataTypes + +from cumulusci.tasks.salesforce import BaseRetrieveMetadata +from defusedxml.minidom import parseString + +class RetrieveMetadataTypes(BaseRetrieveMetadata): + api_2= ApiListMetadataTypes + task_options = { + "api_version": { + "description": "Override the API version used to list metadata" + }, + + } + + def _run_task(self): + + api_object = self.api_2(self, "58.0" ) + root = api_object._get_response().content.decode("utf-8") + + self.logger.info(api_object._process_response(root)) + + + + + \ No newline at end of file diff --git a/cumulusci/tasks/salesforce/__init__.py b/cumulusci/tasks/salesforce/__init__.py index f81c25d557..41756fca2b 100644 --- a/cumulusci/tasks/salesforce/__init__.py +++ b/cumulusci/tasks/salesforce/__init__.py @@ -30,6 +30,7 @@ "PublishCommunity": "cumulusci.tasks.salesforce.PublishCommunity", "RetrievePackaged": "cumulusci.tasks.salesforce.RetrievePackaged", "RetrieveReportsAndDashboards": "cumulusci.tasks.salesforce.RetrieveReportsAndDashboards", + "RetrieveMetadataTypes": "cumulusci.tasks.salesforce.RetrieveMetadataTypes", "RetrieveUnpackaged": "cumulusci.tasks.salesforce.RetrieveUnpackaged", "SOQLQuery": "cumulusci.tasks.salesforce.SOQLQuery", "SetTDTMHandlerStatus": "cumulusci.tasks.salesforce.trigger_handlers", From df59c443f1c165d2374db23c756ad3b99b27758c Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Wed, 4 Oct 2023 16:08:10 +0530 Subject: [PATCH 40/98] task_options --- cci_venv/bin/Activate.ps1 | 241 ++++++++++++++++++ cci_venv/bin/activate | 66 +++++ cci_venv/bin/activate.csh | 25 ++ cci_venv/bin/activate.fish | 64 +++++ cci_venv/bin/black | 8 + cci_venv/bin/blackd | 8 + cci_venv/bin/cci | 8 + cci_venv/bin/chardetect | 8 + cci_venv/bin/coverage | 8 + cci_venv/bin/coverage-3.9 | 8 + cci_venv/bin/coverage3 | 8 + cci_venv/bin/faker | 8 + cci_venv/bin/flake8 | 8 + cci_venv/bin/identify-cli | 8 + cci_venv/bin/isort | 8 + cci_venv/bin/isort-identify-imports | 8 + cci_venv/bin/jsonschema | 8 + cci_venv/bin/keyring | 8 + cci_venv/bin/libdoc | 8 + cci_venv/bin/markdown-it | 8 + cci_venv/bin/myst-anchors | 8 + cci_venv/bin/myst-docutils-html | 8 + cci_venv/bin/myst-docutils-html5 | 8 + cci_venv/bin/myst-docutils-latex | 8 + cci_venv/bin/myst-docutils-pseudoxml | 8 + cci_venv/bin/myst-docutils-xml | 8 + cci_venv/bin/natsort | 8 + cci_venv/bin/nodeenv | 8 + cci_venv/bin/normalizer | 8 + cci_venv/bin/pabot | 8 + cci_venv/bin/pip | 8 + cci_venv/bin/pip-compile | 8 + cci_venv/bin/pip-sync | 8 + cci_venv/bin/pip3 | 8 + cci_venv/bin/pip3.11 | 8 + cci_venv/bin/pip3.9 | 8 + cci_venv/bin/pre-commit | 8 + cci_venv/bin/py.test | 8 + cci_venv/bin/pybabel | 8 + cci_venv/bin/pycodestyle | 8 + cci_venv/bin/pyflakes | 8 + cci_venv/bin/pygmentize | 8 + cci_venv/bin/pyproject-build | 8 + cci_venv/bin/pytest | 8 + cci_venv/bin/python | 1 + cci_venv/bin/python3 | 1 + cci_venv/bin/python3.9 | 1 + cci_venv/bin/rebot | 8 + cci_venv/bin/rflint | 8 + cci_venv/bin/robot | 8 + cci_venv/bin/rst2ansi | 26 ++ cci_venv/bin/rst2html.py | 23 ++ cci_venv/bin/rst2html4.py | 26 ++ cci_venv/bin/rst2html5.py | 35 +++ cci_venv/bin/rst2latex.py | 26 ++ cci_venv/bin/rst2man.py | 26 ++ cci_venv/bin/rst2odt.py | 30 +++ cci_venv/bin/rst2odt_prepstyles.py | 67 +++++ cci_venv/bin/rst2pseudoxml.py | 23 ++ cci_venv/bin/rst2s5.py | 24 ++ cci_venv/bin/rst2xetex.py | 27 ++ cci_venv/bin/rst2xml.py | 23 ++ cci_venv/bin/rstpep2html.py | 25 ++ cci_venv/bin/snowbench | 8 + cci_venv/bin/snowfakery | 8 + cci_venv/bin/sphinx-apidoc | 8 + cci_venv/bin/sphinx-autogen | 8 + cci_venv/bin/sphinx-build | 8 + cci_venv/bin/sphinx-quickstart | 8 + cci_venv/bin/tox | 8 + cci_venv/bin/virtualenv | 8 + cci_venv/bin/wheel | 8 + .../site/python3.9/greenlet/greenlet.h | 164 ++++++++++++ cci_venv/pyvenv.cfg | 3 + cci_venv/requirements/dev.txt | 229 +++++++++++++++++ cci_venv/requirements/prod.txt | 44 ++++ cumulusci/cumulusci.yml | 6 +- cumulusci/salesforce_api/metadata.py | 2 - cumulusci/tasks/metadata_type_list.py | 40 --- .../tasks/salesforce/RetrieveMetadataTypes.py | 19 +- 80 files changed, 1652 insertions(+), 51 deletions(-) create mode 100644 cci_venv/bin/Activate.ps1 create mode 100644 cci_venv/bin/activate create mode 100644 cci_venv/bin/activate.csh create mode 100644 cci_venv/bin/activate.fish create mode 100755 cci_venv/bin/black create mode 100755 cci_venv/bin/blackd create mode 100755 cci_venv/bin/cci create mode 100755 cci_venv/bin/chardetect create mode 100755 cci_venv/bin/coverage create mode 100755 cci_venv/bin/coverage-3.9 create mode 100755 cci_venv/bin/coverage3 create mode 100755 cci_venv/bin/faker create mode 100755 cci_venv/bin/flake8 create mode 100755 cci_venv/bin/identify-cli create mode 100755 cci_venv/bin/isort create mode 100755 cci_venv/bin/isort-identify-imports create mode 100755 cci_venv/bin/jsonschema create mode 100755 cci_venv/bin/keyring create mode 100755 cci_venv/bin/libdoc create mode 100755 cci_venv/bin/markdown-it create mode 100755 cci_venv/bin/myst-anchors create mode 100755 cci_venv/bin/myst-docutils-html create mode 100755 cci_venv/bin/myst-docutils-html5 create mode 100755 cci_venv/bin/myst-docutils-latex create mode 100755 cci_venv/bin/myst-docutils-pseudoxml create mode 100755 cci_venv/bin/myst-docutils-xml create mode 100755 cci_venv/bin/natsort create mode 100755 cci_venv/bin/nodeenv create mode 100755 cci_venv/bin/normalizer create mode 100755 cci_venv/bin/pabot create mode 100755 cci_venv/bin/pip create mode 100755 cci_venv/bin/pip-compile create mode 100755 cci_venv/bin/pip-sync create mode 100755 cci_venv/bin/pip3 create mode 100755 cci_venv/bin/pip3.11 create mode 100755 cci_venv/bin/pip3.9 create mode 100755 cci_venv/bin/pre-commit create mode 100755 cci_venv/bin/py.test create mode 100755 cci_venv/bin/pybabel create mode 100755 cci_venv/bin/pycodestyle create mode 100755 cci_venv/bin/pyflakes create mode 100755 cci_venv/bin/pygmentize create mode 100755 cci_venv/bin/pyproject-build create mode 100755 cci_venv/bin/pytest create mode 120000 cci_venv/bin/python create mode 120000 cci_venv/bin/python3 create mode 120000 cci_venv/bin/python3.9 create mode 100755 cci_venv/bin/rebot create mode 100755 cci_venv/bin/rflint create mode 100755 cci_venv/bin/robot create mode 100755 cci_venv/bin/rst2ansi create mode 100755 cci_venv/bin/rst2html.py create mode 100755 cci_venv/bin/rst2html4.py create mode 100755 cci_venv/bin/rst2html5.py create mode 100755 cci_venv/bin/rst2latex.py create mode 100755 cci_venv/bin/rst2man.py create mode 100755 cci_venv/bin/rst2odt.py create mode 100755 cci_venv/bin/rst2odt_prepstyles.py create mode 100755 cci_venv/bin/rst2pseudoxml.py create mode 100755 cci_venv/bin/rst2s5.py create mode 100755 cci_venv/bin/rst2xetex.py create mode 100755 cci_venv/bin/rst2xml.py create mode 100755 cci_venv/bin/rstpep2html.py create mode 100755 cci_venv/bin/snowbench create mode 100755 cci_venv/bin/snowfakery create mode 100755 cci_venv/bin/sphinx-apidoc create mode 100755 cci_venv/bin/sphinx-autogen create mode 100755 cci_venv/bin/sphinx-build create mode 100755 cci_venv/bin/sphinx-quickstart create mode 100755 cci_venv/bin/tox create mode 100755 cci_venv/bin/virtualenv create mode 100755 cci_venv/bin/wheel create mode 100644 cci_venv/include/site/python3.9/greenlet/greenlet.h create mode 100644 cci_venv/pyvenv.cfg create mode 100644 cci_venv/requirements/dev.txt create mode 100644 cci_venv/requirements/prod.txt delete mode 100644 cumulusci/tasks/metadata_type_list.py diff --git a/cci_venv/bin/Activate.ps1 b/cci_venv/bin/Activate.ps1 new file mode 100644 index 0000000000..2fb3852c3c --- /dev/null +++ b/cci_venv/bin/Activate.ps1 @@ -0,0 +1,241 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/cci_venv/bin/activate b/cci_venv/bin/activate new file mode 100644 index 0000000000..21c2b8d393 --- /dev/null +++ b/cci_venv/bin/activate @@ -0,0 +1,66 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/l.ramireddy/Downloads/CumulusCI/cci_venv" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(cci_venv) ${PS1:-}" + export PS1 +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/cci_venv/bin/activate.csh b/cci_venv/bin/activate.csh new file mode 100644 index 0000000000..b9f287280d --- /dev/null +++ b/cci_venv/bin/activate.csh @@ -0,0 +1,25 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/l.ramireddy/Downloads/CumulusCI/cci_venv" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "(cci_venv) $prompt" +endif + +alias pydoc python -m pydoc + +rehash diff --git a/cci_venv/bin/activate.fish b/cci_venv/bin/activate.fish new file mode 100644 index 0000000000..0725b5f656 --- /dev/null +++ b/cci_venv/bin/activate.fish @@ -0,0 +1,64 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + functions -e fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + + set -e VIRTUAL_ENV + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/l.ramireddy/Downloads/CumulusCI/cci_venv" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "(cci_venv) " (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" +end diff --git a/cci_venv/bin/black b/cci_venv/bin/black new file mode 100755 index 0000000000..8fddae98b0 --- /dev/null +++ b/cci_venv/bin/black @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from black import patched_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(patched_main()) diff --git a/cci_venv/bin/blackd b/cci_venv/bin/blackd new file mode 100755 index 0000000000..452a9818c4 --- /dev/null +++ b/cci_venv/bin/blackd @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from blackd import patched_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(patched_main()) diff --git a/cci_venv/bin/cci b/cci_venv/bin/cci new file mode 100755 index 0000000000..7e3ec88284 --- /dev/null +++ b/cci_venv/bin/cci @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from cumulusci.cli.cci import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/chardetect b/cci_venv/bin/chardetect new file mode 100755 index 0000000000..3e18cdf729 --- /dev/null +++ b/cci_venv/bin/chardetect @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from chardet.cli.chardetect import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/coverage b/cci_venv/bin/coverage new file mode 100755 index 0000000000..3ec554f4f8 --- /dev/null +++ b/cci_venv/bin/coverage @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from coverage.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/coverage-3.9 b/cci_venv/bin/coverage-3.9 new file mode 100755 index 0000000000..3ec554f4f8 --- /dev/null +++ b/cci_venv/bin/coverage-3.9 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from coverage.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/coverage3 b/cci_venv/bin/coverage3 new file mode 100755 index 0000000000..3ec554f4f8 --- /dev/null +++ b/cci_venv/bin/coverage3 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from coverage.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/faker b/cci_venv/bin/faker new file mode 100755 index 0000000000..f4fef2c5bc --- /dev/null +++ b/cci_venv/bin/faker @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from faker.cli import execute_from_command_line +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(execute_from_command_line()) diff --git a/cci_venv/bin/flake8 b/cci_venv/bin/flake8 new file mode 100755 index 0000000000..28bccc742c --- /dev/null +++ b/cci_venv/bin/flake8 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from flake8.main.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/identify-cli b/cci_venv/bin/identify-cli new file mode 100755 index 0000000000..a15a809bc2 --- /dev/null +++ b/cci_venv/bin/identify-cli @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from identify.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/isort b/cci_venv/bin/isort new file mode 100755 index 0000000000..63eae32e31 --- /dev/null +++ b/cci_venv/bin/isort @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from isort.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/isort-identify-imports b/cci_venv/bin/isort-identify-imports new file mode 100755 index 0000000000..b0d648fb3f --- /dev/null +++ b/cci_venv/bin/isort-identify-imports @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from isort.main import identify_imports_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(identify_imports_main()) diff --git a/cci_venv/bin/jsonschema b/cci_venv/bin/jsonschema new file mode 100755 index 0000000000..7e071f8116 --- /dev/null +++ b/cci_venv/bin/jsonschema @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jsonschema.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/keyring b/cci_venv/bin/keyring new file mode 100755 index 0000000000..0e1bf44fed --- /dev/null +++ b/cci_venv/bin/keyring @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from keyring.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/libdoc b/cci_venv/bin/libdoc new file mode 100755 index 0000000000..e5ed24b522 --- /dev/null +++ b/cci_venv/bin/libdoc @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from robot.libdoc import libdoc_cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(libdoc_cli()) diff --git a/cci_venv/bin/markdown-it b/cci_venv/bin/markdown-it new file mode 100755 index 0000000000..f5d45d06be --- /dev/null +++ b/cci_venv/bin/markdown-it @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from markdown_it.cli.parse import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/myst-anchors b/cci_venv/bin/myst-anchors new file mode 100755 index 0000000000..77e53cbd7b --- /dev/null +++ b/cci_venv/bin/myst-anchors @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from myst_parser.cli import print_anchors +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(print_anchors()) diff --git a/cci_venv/bin/myst-docutils-html b/cci_venv/bin/myst-docutils-html new file mode 100755 index 0000000000..1d8127474e --- /dev/null +++ b/cci_venv/bin/myst-docutils-html @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from myst_parser.parsers.docutils_ import cli_html +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_html()) diff --git a/cci_venv/bin/myst-docutils-html5 b/cci_venv/bin/myst-docutils-html5 new file mode 100755 index 0000000000..b08822ba1d --- /dev/null +++ b/cci_venv/bin/myst-docutils-html5 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from myst_parser.parsers.docutils_ import cli_html5 +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_html5()) diff --git a/cci_venv/bin/myst-docutils-latex b/cci_venv/bin/myst-docutils-latex new file mode 100755 index 0000000000..f694b85aff --- /dev/null +++ b/cci_venv/bin/myst-docutils-latex @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from myst_parser.parsers.docutils_ import cli_latex +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_latex()) diff --git a/cci_venv/bin/myst-docutils-pseudoxml b/cci_venv/bin/myst-docutils-pseudoxml new file mode 100755 index 0000000000..32112a5931 --- /dev/null +++ b/cci_venv/bin/myst-docutils-pseudoxml @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from myst_parser.parsers.docutils_ import cli_pseudoxml +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_pseudoxml()) diff --git a/cci_venv/bin/myst-docutils-xml b/cci_venv/bin/myst-docutils-xml new file mode 100755 index 0000000000..b2ba7b6267 --- /dev/null +++ b/cci_venv/bin/myst-docutils-xml @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from myst_parser.parsers.docutils_ import cli_xml +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_xml()) diff --git a/cci_venv/bin/natsort b/cci_venv/bin/natsort new file mode 100755 index 0000000000..6f4b97538a --- /dev/null +++ b/cci_venv/bin/natsort @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from natsort.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/nodeenv b/cci_venv/bin/nodeenv new file mode 100755 index 0000000000..1a322f2f46 --- /dev/null +++ b/cci_venv/bin/nodeenv @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from nodeenv import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/normalizer b/cci_venv/bin/normalizer new file mode 100755 index 0000000000..84beb4cfa6 --- /dev/null +++ b/cci_venv/bin/normalizer @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer.cli.normalizer import cli_detect +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_detect()) diff --git a/cci_venv/bin/pabot b/cci_venv/bin/pabot new file mode 100755 index 0000000000..1c422a1396 --- /dev/null +++ b/cci_venv/bin/pabot @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pabot.pabot import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pip b/cci_venv/bin/pip new file mode 100755 index 0000000000..e6124a1a8b --- /dev/null +++ b/cci_venv/bin/pip @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pip-compile b/cci_venv/bin/pip-compile new file mode 100755 index 0000000000..93a7025865 --- /dev/null +++ b/cci_venv/bin/pip-compile @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from piptools.scripts.compile import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/cci_venv/bin/pip-sync b/cci_venv/bin/pip-sync new file mode 100755 index 0000000000..d2aab886e6 --- /dev/null +++ b/cci_venv/bin/pip-sync @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from piptools.scripts.sync import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/cci_venv/bin/pip3 b/cci_venv/bin/pip3 new file mode 100755 index 0000000000..e6124a1a8b --- /dev/null +++ b/cci_venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pip3.11 b/cci_venv/bin/pip3.11 new file mode 100755 index 0000000000..e6124a1a8b --- /dev/null +++ b/cci_venv/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pip3.9 b/cci_venv/bin/pip3.9 new file mode 100755 index 0000000000..e6124a1a8b --- /dev/null +++ b/cci_venv/bin/pip3.9 @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pre-commit b/cci_venv/bin/pre-commit new file mode 100755 index 0000000000..49e634db94 --- /dev/null +++ b/cci_venv/bin/pre-commit @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pre_commit.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/py.test b/cci_venv/bin/py.test new file mode 100755 index 0000000000..89d65fe7d7 --- /dev/null +++ b/cci_venv/bin/py.test @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pytest import console_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(console_main()) diff --git a/cci_venv/bin/pybabel b/cci_venv/bin/pybabel new file mode 100755 index 0000000000..c8556b4286 --- /dev/null +++ b/cci_venv/bin/pybabel @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from babel.messages.frontend import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pycodestyle b/cci_venv/bin/pycodestyle new file mode 100755 index 0000000000..78806a67b0 --- /dev/null +++ b/cci_venv/bin/pycodestyle @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pycodestyle import _main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(_main()) diff --git a/cci_venv/bin/pyflakes b/cci_venv/bin/pyflakes new file mode 100755 index 0000000000..411cd3b657 --- /dev/null +++ b/cci_venv/bin/pyflakes @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pyflakes.api import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pygmentize b/cci_venv/bin/pygmentize new file mode 100755 index 0000000000..acd74380f3 --- /dev/null +++ b/cci_venv/bin/pygmentize @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pygments.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/pyproject-build b/cci_venv/bin/pyproject-build new file mode 100755 index 0000000000..a6ce606f38 --- /dev/null +++ b/cci_venv/bin/pyproject-build @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from build.__main__ import entrypoint +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(entrypoint()) diff --git a/cci_venv/bin/pytest b/cci_venv/bin/pytest new file mode 100755 index 0000000000..89d65fe7d7 --- /dev/null +++ b/cci_venv/bin/pytest @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pytest import console_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(console_main()) diff --git a/cci_venv/bin/python b/cci_venv/bin/python new file mode 120000 index 0000000000..b8a0adbbb9 --- /dev/null +++ b/cci_venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/cci_venv/bin/python3 b/cci_venv/bin/python3 new file mode 120000 index 0000000000..f25545feec --- /dev/null +++ b/cci_venv/bin/python3 @@ -0,0 +1 @@ +/Library/Developer/CommandLineTools/usr/bin/python3 \ No newline at end of file diff --git a/cci_venv/bin/python3.9 b/cci_venv/bin/python3.9 new file mode 120000 index 0000000000..b8a0adbbb9 --- /dev/null +++ b/cci_venv/bin/python3.9 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/cci_venv/bin/rebot b/cci_venv/bin/rebot new file mode 100755 index 0000000000..0f81b389f7 --- /dev/null +++ b/cci_venv/bin/rebot @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from robot.rebot import rebot_cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(rebot_cli()) diff --git a/cci_venv/bin/rflint b/cci_venv/bin/rflint new file mode 100755 index 0000000000..e649a3e7b1 --- /dev/null +++ b/cci_venv/bin/rflint @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from rflint.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/robot b/cci_venv/bin/robot new file mode 100755 index 0000000000..d8ca923a58 --- /dev/null +++ b/cci_venv/bin/robot @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from robot.run import run_cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run_cli()) diff --git a/cci_venv/bin/rst2ansi b/cci_venv/bin/rst2ansi new file mode 100755 index 0000000000..631cb26005 --- /dev/null +++ b/cci_venv/bin/rst2ansi @@ -0,0 +1,26 @@ +#!/usr/bin/python + +import sys +from rst2ansi import rst2ansi +import argparse +import io + +parser = argparse.ArgumentParser(description='Prints a reStructuredText input in an ansi-decorated format suitable for console output.') +parser.add_argument('file', type=str, nargs='?', help='A path to the file to open') + +args = parser.parse_args() + +def process_file(f): + out = rst2ansi(f.read()) + if out: + try: + print(out) + except UnicodeEncodeError: + print(out.encode('ascii', errors='backslashreplace').decode('ascii')) + +if args.file: + with io.open(args.file, 'rb') as f: + process_file(f) +else: + process_file(sys.stdin) + diff --git a/cci_venv/bin/rst2html.py b/cci_venv/bin/rst2html.py new file mode 100755 index 0000000000..4deeb673e1 --- /dev/null +++ b/cci_venv/bin/rst2html.py @@ -0,0 +1,23 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2html.py 4564 2006-05-21 20:44:42Z wiemann $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing HTML. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description + + +description = ('Generates (X)HTML documents from standalone reStructuredText ' + 'sources. ' + default_description) + +publish_cmdline(writer_name='html', description=description) diff --git a/cci_venv/bin/rst2html4.py b/cci_venv/bin/rst2html4.py new file mode 100755 index 0000000000..26b7083fdf --- /dev/null +++ b/cci_venv/bin/rst2html4.py @@ -0,0 +1,26 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2html4.py 7994 2016-12-10 17:41:45Z milde $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing (X)HTML. + +The output conforms to XHTML 1.0 transitional +and almost to HTML 4.01 transitional (except for closing empty tags). +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description + + +description = ('Generates (X)HTML documents from standalone reStructuredText ' + 'sources. ' + default_description) + +publish_cmdline(writer_name='html4', description=description) diff --git a/cci_venv/bin/rst2html5.py b/cci_venv/bin/rst2html5.py new file mode 100755 index 0000000000..07bb91cc24 --- /dev/null +++ b/cci_venv/bin/rst2html5.py @@ -0,0 +1,35 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf8 -*- +# :Copyright: © 2015 Günter Milde. +# :License: Released under the terms of the `2-Clause BSD license`_, in short: +# +# Copying and distribution of this file, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. +# This file is offered as-is, without any warranty. +# +# .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause +# +# Revision: $Revision: 8410 $ +# Date: $Date: 2019-11-04 22:14:43 +0100 (Mo, 04. Nov 2019) $ + +""" +A minimal front end to the Docutils Publisher, producing HTML 5 documents. + +The output also conforms to XHTML 1.0 transitional +(except for the doctype declaration). +""" + +try: + import locale # module missing in Jython + locale.setlocale(locale.LC_ALL, '') +except locale.Error: + pass + +from docutils.core import publish_cmdline, default_description + +description = (u'Generates HTML 5 documents from standalone ' + u'reStructuredText sources ' + + default_description) + +publish_cmdline(writer_name='html5', description=description) diff --git a/cci_venv/bin/rst2latex.py b/cci_venv/bin/rst2latex.py new file mode 100755 index 0000000000..9dcd011a95 --- /dev/null +++ b/cci_venv/bin/rst2latex.py @@ -0,0 +1,26 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2latex.py 5905 2009-04-16 12:04:49Z milde $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing LaTeX. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline + +description = ('Generates LaTeX documents from standalone reStructuredText ' + 'sources. ' + 'Reads from (default is stdin) and writes to ' + ' (default is stdout). See ' + ' for ' + 'the full reference.') + +publish_cmdline(writer_name='latex', description=description) diff --git a/cci_venv/bin/rst2man.py b/cci_venv/bin/rst2man.py new file mode 100755 index 0000000000..3c40200330 --- /dev/null +++ b/cci_venv/bin/rst2man.py @@ -0,0 +1,26 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# Author: +# Contact: grubert@users.sf.net +# Copyright: This module has been placed in the public domain. + +""" +man.py +====== + +This module provides a simple command line interface that uses the +man page writer to output from ReStructuredText source. +""" + +import locale +try: + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description +from docutils.writers import manpage + +description = ("Generates plain unix manual documents. " + default_description) + +publish_cmdline(writer=manpage.Writer(), description=description) diff --git a/cci_venv/bin/rst2odt.py b/cci_venv/bin/rst2odt.py new file mode 100755 index 0000000000..0be802f64b --- /dev/null +++ b/cci_venv/bin/rst2odt.py @@ -0,0 +1,30 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2odt.py 5839 2009-01-07 19:09:28Z dkuhlman $ +# Author: Dave Kuhlman +# Copyright: This module has been placed in the public domain. + +""" +A front end to the Docutils Publisher, producing OpenOffice documents. +""" + +import sys +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline_to_binary, default_description +from docutils.writers.odf_odt import Writer, Reader + + +description = ('Generates OpenDocument/OpenOffice/ODF documents from ' + 'standalone reStructuredText sources. ' + default_description) + + +writer = Writer() +reader = Reader() +output = publish_cmdline_to_binary(reader=reader, writer=writer, + description=description) + diff --git a/cci_venv/bin/rst2odt_prepstyles.py b/cci_venv/bin/rst2odt_prepstyles.py new file mode 100755 index 0000000000..727e87f91d --- /dev/null +++ b/cci_venv/bin/rst2odt_prepstyles.py @@ -0,0 +1,67 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2odt_prepstyles.py 8346 2019-08-26 12:11:32Z milde $ +# Author: Dave Kuhlman +# Copyright: This module has been placed in the public domain. + +""" +Fix a word-processor-generated styles.odt for odtwriter use: Drop page size +specifications from styles.xml in STYLE_FILE.odt. +""" + +# Author: Michael Schutte + +from __future__ import print_function + +from lxml import etree +import sys +import zipfile +from tempfile import mkstemp +import shutil +import os + +NAMESPACES = { + "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", + "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" +} + + +def prepstyle(filename): + + zin = zipfile.ZipFile(filename) + styles = zin.read("styles.xml") + + root = etree.fromstring(styles) + for el in root.xpath("//style:page-layout-properties", + namespaces=NAMESPACES): + for attr in el.attrib: + if attr.startswith("{%s}" % NAMESPACES["fo"]): + del el.attrib[attr] + + tempname = mkstemp() + zout = zipfile.ZipFile(os.fdopen(tempname[0], "w"), "w", + zipfile.ZIP_DEFLATED) + + for item in zin.infolist(): + if item.filename == "styles.xml": + zout.writestr(item, etree.tostring(root)) + else: + zout.writestr(item, zin.read(item.filename)) + + zout.close() + zin.close() + shutil.move(tempname[1], filename) + + +def main(): + args = sys.argv[1:] + if len(args) != 1: + print(__doc__, file=sys.stderr) + print("Usage: %s STYLE_FILE.odt\n" % sys.argv[0], file=sys.stderr) + sys.exit(1) + filename = args[0] + prepstyle(filename) + + +if __name__ == '__main__': + main() diff --git a/cci_venv/bin/rst2pseudoxml.py b/cci_venv/bin/rst2pseudoxml.py new file mode 100755 index 0000000000..717bf2e1ea --- /dev/null +++ b/cci_venv/bin/rst2pseudoxml.py @@ -0,0 +1,23 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2pseudoxml.py 4564 2006-05-21 20:44:42Z wiemann $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing pseudo-XML. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description + + +description = ('Generates pseudo-XML from standalone reStructuredText ' + 'sources (for testing purposes). ' + default_description) + +publish_cmdline(description=description) diff --git a/cci_venv/bin/rst2s5.py b/cci_venv/bin/rst2s5.py new file mode 100755 index 0000000000..196249d292 --- /dev/null +++ b/cci_venv/bin/rst2s5.py @@ -0,0 +1,24 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2s5.py 4564 2006-05-21 20:44:42Z wiemann $ +# Author: Chris Liechti +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing HTML slides using +the S5 template system. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description + + +description = ('Generates S5 (X)HTML slideshow documents from standalone ' + 'reStructuredText sources. ' + default_description) + +publish_cmdline(writer_name='s5', description=description) diff --git a/cci_venv/bin/rst2xetex.py b/cci_venv/bin/rst2xetex.py new file mode 100755 index 0000000000..5c50c3f7b8 --- /dev/null +++ b/cci_venv/bin/rst2xetex.py @@ -0,0 +1,27 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2xetex.py 7847 2015-03-17 17:30:47Z milde $ +# Author: Guenter Milde +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing Lua/XeLaTeX code. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline + +description = ('Generates LaTeX documents from standalone reStructuredText ' + 'sources for compilation with the Unicode-aware TeX variants ' + 'XeLaTeX or LuaLaTeX. ' + 'Reads from (default is stdin) and writes to ' + ' (default is stdout). See ' + ' for ' + 'the full reference.') + +publish_cmdline(writer_name='xetex', description=description) diff --git a/cci_venv/bin/rst2xml.py b/cci_venv/bin/rst2xml.py new file mode 100755 index 0000000000..db68e2f214 --- /dev/null +++ b/cci_venv/bin/rst2xml.py @@ -0,0 +1,23 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rst2xml.py 4564 2006-05-21 20:44:42Z wiemann $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing Docutils XML. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description + + +description = ('Generates Docutils-native XML from standalone ' + 'reStructuredText sources. ' + default_description) + +publish_cmdline(writer_name='xml', description=description) diff --git a/cci_venv/bin/rstpep2html.py b/cci_venv/bin/rstpep2html.py new file mode 100755 index 0000000000..4fb4508aaa --- /dev/null +++ b/cci_venv/bin/rstpep2html.py @@ -0,0 +1,25 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python + +# $Id: rstpep2html.py 4564 2006-05-21 20:44:42Z wiemann $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A minimal front end to the Docutils Publisher, producing HTML from PEP +(Python Enhancement Proposal) documents. +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +from docutils.core import publish_cmdline, default_description + + +description = ('Generates (X)HTML from reStructuredText-format PEP files. ' + + default_description) + +publish_cmdline(reader_name='pep', writer_name='pep_html', + description=description) diff --git a/cci_venv/bin/snowbench b/cci_venv/bin/snowbench new file mode 100755 index 0000000000..e75bb04614 --- /dev/null +++ b/cci_venv/bin/snowbench @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from snowfakery.tools.snowbench import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/snowfakery b/cci_venv/bin/snowfakery new file mode 100755 index 0000000000..8bfe15089b --- /dev/null +++ b/cci_venv/bin/snowfakery @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from snowfakery.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/sphinx-apidoc b/cci_venv/bin/sphinx-apidoc new file mode 100755 index 0000000000..fd351c9e85 --- /dev/null +++ b/cci_venv/bin/sphinx-apidoc @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from sphinx.ext.apidoc import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/sphinx-autogen b/cci_venv/bin/sphinx-autogen new file mode 100755 index 0000000000..4f605a06bb --- /dev/null +++ b/cci_venv/bin/sphinx-autogen @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from sphinx.ext.autosummary.generate import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/sphinx-build b/cci_venv/bin/sphinx-build new file mode 100755 index 0000000000..a4d9d78528 --- /dev/null +++ b/cci_venv/bin/sphinx-build @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from sphinx.cmd.build import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/sphinx-quickstart b/cci_venv/bin/sphinx-quickstart new file mode 100755 index 0000000000..8b8f570daa --- /dev/null +++ b/cci_venv/bin/sphinx-quickstart @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from sphinx.cmd.quickstart import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/bin/tox b/cci_venv/bin/tox new file mode 100755 index 0000000000..e288d3a32d --- /dev/null +++ b/cci_venv/bin/tox @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from tox.run import run +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run()) diff --git a/cci_venv/bin/virtualenv b/cci_venv/bin/virtualenv new file mode 100755 index 0000000000..e22135d441 --- /dev/null +++ b/cci_venv/bin/virtualenv @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from virtualenv.__main__ import run_with_catch +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run_with_catch()) diff --git a/cci_venv/bin/wheel b/cci_venv/bin/wheel new file mode 100755 index 0000000000..ac53361d94 --- /dev/null +++ b/cci_venv/bin/wheel @@ -0,0 +1,8 @@ +#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from wheel.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/cci_venv/include/site/python3.9/greenlet/greenlet.h b/cci_venv/include/site/python3.9/greenlet/greenlet.h new file mode 100644 index 0000000000..d02a16e434 --- /dev/null +++ b/cci_venv/include/site/python3.9/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/cci_venv/pyvenv.cfg b/cci_venv/pyvenv.cfg new file mode 100644 index 0000000000..4760c1ffc4 --- /dev/null +++ b/cci_venv/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /Library/Developer/CommandLineTools/usr/bin +include-system-site-packages = false +version = 3.9.6 diff --git a/cci_venv/requirements/dev.txt b/cci_venv/requirements/dev.txt new file mode 100644 index 0000000000..6af1da5af7 --- /dev/null +++ b/cci_venv/requirements/dev.txt @@ -0,0 +1,229 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe requirements/dev.in +# +attrs==22.2.0 + # via + # jsonschema + # pytest +black==23.1.0 + # via -r requirements/dev.in +cachetools==5.3.0 + # via tox +certifi==2022.12.7 + # via + # -r requirements/prod.txt + # requests +cfgv==3.3.1 + # via pre-commit +chardet==5.1.0 + # via + # diff-cover + # tox +charset-normalizer==3.0.1 + # via + # -r requirements/prod.txt + # requests +click==8.1.3 + # via + # -r requirements/prod.txt + # black + # mkdocs +colorama==0.4.6 + # via tox +coverage[toml]==6.5.0 + # via + # -r requirements/dev.in + # coveralls + # pytest-cov +coveralls==3.3.1 + # via -r requirements/dev.in +diff-cover==7.4.0 + # via -r requirements/dev.in +distlib==0.3.6 + # via virtualenv +docopt==0.6.2 + # via coveralls +exceptiongroup==1.1.0 + # via pytest +faker==17.0.0 + # via + # -r requirements/prod.txt + # faker-microservice +faker-microservice==2.0.0 + # via -r requirements/dev.in +filelock==3.9.0 + # via + # tox + # virtualenv +ghp-import==2.1.0 + # via mkdocs +greenlet==2.0.2 + # via + # -r requirements/prod.txt + # sqlalchemy +gvgen==1.0 + # via -r requirements/prod.txt +identify==2.5.18 + # via pre-commit +idna==3.4 + # via + # -r requirements/prod.txt + # requests + # yarl +importlib-metadata==6.0.0 + # via + # markdown + # mkdocs +iniconfig==2.0.0 + # via pytest +jinja2==3.1.2 + # via + # -r requirements/prod.txt + # diff-cover + # mkdocs +jsonschema==4.17.3 + # via -r requirements/dev.in +markdown==3.4.1 + # via mkdocs +markupsafe==2.1.2 + # via + # -r requirements/prod.txt + # jinja2 +mergedeep==1.3.4 + # via mkdocs +mkdocs==1.2.4 + # via + # -r requirements/dev.in + # mkdocs-exclude-search +mkdocs-exclude-search==0.6.5 + # via -r requirements/dev.in +multidict==6.0.4 + # via yarl +mypy-extensions==1.0.0 + # via black +nodeenv==1.7.0 + # via + # pre-commit + # pyright +packaging==23.0 + # via + # black + # mkdocs + # pyproject-api + # pytest + # tox +pathspec==0.11.0 + # via black +platformdirs==3.0.0 + # via + # black + # tox + # virtualenv +pluggy==1.0.0 + # via + # diff-cover + # pytest + # tox +pre-commit==3.0.4 + # via -r requirements/dev.in +pydantic==1.10.5 + # via -r requirements/prod.txt +pygments==2.14.0 + # via diff-cover +pyproject-api==1.5.0 + # via tox +pyright==1.1.294 + # via -r requirements/dev.in +pyrsistent==0.19.3 + # via jsonschema +pytest==7.2.1 + # via + # -r requirements/dev.in + # pytest-cov + # pytest-vcr +pytest-cov==4.0.0 + # via -r requirements/dev.in +pytest-vcr==1.0.2 + # via -r requirements/dev.in +python-baseconv==1.2.2 + # via -r requirements/prod.txt +python-dateutil==2.8.2 + # via + # -r requirements/prod.txt + # faker + # ghp-import +pyyaml==6.0 + # via + # -r requirements/prod.txt + # mkdocs + # pre-commit + # pyyaml-env-tag + # vcrpy +pyyaml-env-tag==0.1 + # via mkdocs +requests==2.28.2 + # via + # -r requirements/prod.txt + # coveralls + # responses +responses==0.22.0 + # via -r requirements/dev.in +six==1.16.0 + # via + # -r requirements/prod.txt + # python-dateutil + # vcrpy +sqlalchemy==1.4.46 + # via -r requirements/prod.txt +toml==0.10.2 + # via responses +tomli==2.0.1 + # via + # black + # coverage + # pyproject-api + # pytest + # tox +tox==4.4.5 + # via + # -r requirements/dev.in + # tox-gh-actions +tox-gh-actions==3.0.0 + # via -r requirements/dev.in +typeguard==2.13.3 + # via -r requirements/dev.in +types-toml==0.10.8.4 + # via responses +typing-extensions==4.5.0 + # via + # -r requirements/prod.txt + # black + # pydantic +urllib3==1.26.14 + # via + # -r requirements/prod.txt + # requests + # responses +vcrpy==4.2.1 + # via + # -r requirements/dev.in + # pytest-vcr +virtualenv==20.19.0 + # via + # pre-commit + # tox +watchdog==2.2.1 + # via mkdocs +wrapt==1.14.1 + # via vcrpy +yarl==1.8.2 + # via vcrpy +zipp==3.13.0 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +setuptools==67.3.2 + # via nodeenv diff --git a/cci_venv/requirements/prod.txt b/cci_venv/requirements/prod.txt new file mode 100644 index 0000000000..affd25463e --- /dev/null +++ b/cci_venv/requirements/prod.txt @@ -0,0 +1,44 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe requirements/prod.in +# +certifi==2022.12.7 + # via requests +charset-normalizer==3.0.1 + # via requests +click==8.1.3 + # via -r requirements/prod.in +faker==17.0.0 + # via -r requirements/prod.in +greenlet==2.0.2 + # via sqlalchemy +gvgen==1.0 + # via -r requirements/prod.in +idna==3.4 + # via requests +jinja2==3.1.2 + # via -r requirements/prod.in +markupsafe==2.1.2 + # via jinja2 +pydantic==1.10.5 + # via -r requirements/prod.in +python-baseconv==1.2.2 + # via -r requirements/prod.in +python-dateutil==2.8.2 + # via + # -r requirements/prod.in + # faker +pyyaml==6.0 + # via -r requirements/prod.in +requests==2.28.2 + # via -r requirements/prod.in +six==1.16.0 + # via python-dateutil +sqlalchemy==1.4.46 + # via -r requirements/prod.in +typing-extensions==4.5.0 + # via pydantic +urllib3==1.26.14 + # via requests diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 5f8fb787ae..fbe9493ebc 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -8,10 +8,6 @@ tasks: options: status: True - metadata_types: - class_path: cumulusci.tasks.metadata_type_list.MetadataTypeList - - deactivate_flow: group: Metadata Transformations description: deactivates Flows identified by a given list of Developer Names @@ -484,7 +480,9 @@ tasks: retrieve_metadatatypes: class_path: cumulusci.tasks.salesforce.RetrieveMetadataTypes + description: Retrieves the metadata types supported by the org based on the api version group: Salesforce Metadatatypes + retrieve_src: description: Retrieves the packaged metadata into the src directory class_path: cumulusci.tasks.salesforce.RetrievePackaged diff --git a/cumulusci/salesforce_api/metadata.py b/cumulusci/salesforce_api/metadata.py index ad02aff976..c95fd29783 100644 --- a/cumulusci/salesforce_api/metadata.py +++ b/cumulusci/salesforce_api/metadata.py @@ -696,8 +696,6 @@ def __init__( else task.project_config.project__package__api_version ) self.api_version = self.as_of_version - - def _build_envelope_start(self): diff --git a/cumulusci/tasks/metadata_type_list.py b/cumulusci/tasks/metadata_type_list.py deleted file mode 100644 index 83b0631bca..0000000000 --- a/cumulusci/tasks/metadata_type_list.py +++ /dev/null @@ -1,40 +0,0 @@ -import sarge -import json -from cumulusci.core.tasks import BaseTask -from cumulusci.core.sfdx import sfdx - -class MetadataTypeList(BaseTask): - - task_options = { - "org_username": { - "description": "Username for the org", - "required":True, - }, - } - - def _run_task(self): - - p: sarge.Command = sfdx( - f"force mdapi describemetadata --json ", - - username=self.options["org_username"], - ) - stdout = p.stdout_text.read() - - self.metadata_list=[] - - if p.returncode: - self.logger.error( f"Couldn't load list of Metadata types: \n{stdout}") - - else: - data=json.loads(stdout) - self.logger.info("List of Metadata types enabled for the org : ") - - for x in data['result']['metadataObjects']: - self.metadata_list.append(x['xmlName']) - self.metadata_list+=x['childXmlNames'] - - self.logger.info(self.metadata_list) - - - \ No newline at end of file diff --git a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py index 96622b7316..aa57ec749a 100644 --- a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py +++ b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py @@ -5,19 +5,28 @@ from defusedxml.minidom import parseString class RetrieveMetadataTypes(BaseRetrieveMetadata): - api_2= ApiListMetadataTypes + api_class= ApiListMetadataTypes task_options = { "api_version": { - "description": "Override the API version used to list metadata" + "description": "Override the API version used to list metadatatypes" }, } + def _init_options(self, kwargs): + super(RetrieveMetadataTypes, self)._init_options(kwargs) + if "api_version" not in self.options: + self.options[ + "api_version" + ] = self.project_config.project__package__api_version - def _run_task(self): + def _get_api(self): + return self.api_class( + self, self.options.get("api_version") + ) - api_object = self.api_2(self, "58.0" ) + def _run_task(self): + api_object = self._get_api() root = api_object._get_response().content.decode("utf-8") - self.logger.info(api_object._process_response(root)) From 8cfba34ebc7383913d3e8a285334b57afdaa70ac Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Wed, 4 Oct 2023 16:11:15 +0530 Subject: [PATCH 41/98] task_options --- cci_venv/bin/Activate.ps1 | 241 ------------------ cci_venv/bin/activate | 66 ----- cci_venv/bin/activate.csh | 25 -- cci_venv/bin/activate.fish | 64 ----- cci_venv/bin/black | 8 - cci_venv/bin/blackd | 8 - cci_venv/bin/cci | 8 - cci_venv/bin/chardetect | 8 - cci_venv/bin/coverage | 8 - cci_venv/bin/coverage-3.9 | 8 - cci_venv/bin/coverage3 | 8 - cci_venv/bin/faker | 8 - cci_venv/bin/flake8 | 8 - cci_venv/bin/identify-cli | 8 - cci_venv/bin/isort | 8 - cci_venv/bin/isort-identify-imports | 8 - cci_venv/bin/jsonschema | 8 - cci_venv/bin/keyring | 8 - cci_venv/bin/libdoc | 8 - cci_venv/bin/markdown-it | 8 - cci_venv/bin/myst-anchors | 8 - cci_venv/bin/myst-docutils-html | 8 - cci_venv/bin/myst-docutils-html5 | 8 - cci_venv/bin/myst-docutils-latex | 8 - cci_venv/bin/myst-docutils-pseudoxml | 8 - cci_venv/bin/myst-docutils-xml | 8 - cci_venv/bin/natsort | 8 - cci_venv/bin/nodeenv | 8 - cci_venv/bin/normalizer | 8 - cci_venv/bin/pabot | 8 - cci_venv/bin/pip | 8 - cci_venv/bin/pip-compile | 8 - cci_venv/bin/pip-sync | 8 - cci_venv/bin/pip3 | 8 - cci_venv/bin/pip3.11 | 8 - cci_venv/bin/pip3.9 | 8 - cci_venv/bin/pre-commit | 8 - cci_venv/bin/py.test | 8 - cci_venv/bin/pybabel | 8 - cci_venv/bin/pycodestyle | 8 - cci_venv/bin/pyflakes | 8 - cci_venv/bin/pygmentize | 8 - cci_venv/bin/pyproject-build | 8 - cci_venv/bin/pytest | 8 - cci_venv/bin/python | 1 - cci_venv/bin/python3 | 1 - cci_venv/bin/python3.9 | 1 - cci_venv/bin/rebot | 8 - cci_venv/bin/rflint | 8 - cci_venv/bin/robot | 8 - cci_venv/bin/rst2ansi | 26 -- cci_venv/bin/rst2html.py | 23 -- cci_venv/bin/rst2html4.py | 26 -- cci_venv/bin/rst2html5.py | 35 --- cci_venv/bin/rst2latex.py | 26 -- cci_venv/bin/rst2man.py | 26 -- cci_venv/bin/rst2odt.py | 30 --- cci_venv/bin/rst2odt_prepstyles.py | 67 ----- cci_venv/bin/rst2pseudoxml.py | 23 -- cci_venv/bin/rst2s5.py | 24 -- cci_venv/bin/rst2xetex.py | 27 -- cci_venv/bin/rst2xml.py | 23 -- cci_venv/bin/rstpep2html.py | 25 -- cci_venv/bin/snowbench | 8 - cci_venv/bin/snowfakery | 8 - cci_venv/bin/sphinx-apidoc | 8 - cci_venv/bin/sphinx-autogen | 8 - cci_venv/bin/sphinx-build | 8 - cci_venv/bin/sphinx-quickstart | 8 - cci_venv/bin/tox | 8 - cci_venv/bin/virtualenv | 8 - cci_venv/bin/wheel | 8 - .../site/python3.9/greenlet/greenlet.h | 164 ------------ cci_venv/pyvenv.cfg | 3 - cci_venv/requirements/dev.txt | 229 ----------------- cci_venv/requirements/prod.txt | 44 ---- 76 files changed, 1636 deletions(-) delete mode 100644 cci_venv/bin/Activate.ps1 delete mode 100644 cci_venv/bin/activate delete mode 100644 cci_venv/bin/activate.csh delete mode 100644 cci_venv/bin/activate.fish delete mode 100755 cci_venv/bin/black delete mode 100755 cci_venv/bin/blackd delete mode 100755 cci_venv/bin/cci delete mode 100755 cci_venv/bin/chardetect delete mode 100755 cci_venv/bin/coverage delete mode 100755 cci_venv/bin/coverage-3.9 delete mode 100755 cci_venv/bin/coverage3 delete mode 100755 cci_venv/bin/faker delete mode 100755 cci_venv/bin/flake8 delete mode 100755 cci_venv/bin/identify-cli delete mode 100755 cci_venv/bin/isort delete mode 100755 cci_venv/bin/isort-identify-imports delete mode 100755 cci_venv/bin/jsonschema delete mode 100755 cci_venv/bin/keyring delete mode 100755 cci_venv/bin/libdoc delete mode 100755 cci_venv/bin/markdown-it delete mode 100755 cci_venv/bin/myst-anchors delete mode 100755 cci_venv/bin/myst-docutils-html delete mode 100755 cci_venv/bin/myst-docutils-html5 delete mode 100755 cci_venv/bin/myst-docutils-latex delete mode 100755 cci_venv/bin/myst-docutils-pseudoxml delete mode 100755 cci_venv/bin/myst-docutils-xml delete mode 100755 cci_venv/bin/natsort delete mode 100755 cci_venv/bin/nodeenv delete mode 100755 cci_venv/bin/normalizer delete mode 100755 cci_venv/bin/pabot delete mode 100755 cci_venv/bin/pip delete mode 100755 cci_venv/bin/pip-compile delete mode 100755 cci_venv/bin/pip-sync delete mode 100755 cci_venv/bin/pip3 delete mode 100755 cci_venv/bin/pip3.11 delete mode 100755 cci_venv/bin/pip3.9 delete mode 100755 cci_venv/bin/pre-commit delete mode 100755 cci_venv/bin/py.test delete mode 100755 cci_venv/bin/pybabel delete mode 100755 cci_venv/bin/pycodestyle delete mode 100755 cci_venv/bin/pyflakes delete mode 100755 cci_venv/bin/pygmentize delete mode 100755 cci_venv/bin/pyproject-build delete mode 100755 cci_venv/bin/pytest delete mode 120000 cci_venv/bin/python delete mode 120000 cci_venv/bin/python3 delete mode 120000 cci_venv/bin/python3.9 delete mode 100755 cci_venv/bin/rebot delete mode 100755 cci_venv/bin/rflint delete mode 100755 cci_venv/bin/robot delete mode 100755 cci_venv/bin/rst2ansi delete mode 100755 cci_venv/bin/rst2html.py delete mode 100755 cci_venv/bin/rst2html4.py delete mode 100755 cci_venv/bin/rst2html5.py delete mode 100755 cci_venv/bin/rst2latex.py delete mode 100755 cci_venv/bin/rst2man.py delete mode 100755 cci_venv/bin/rst2odt.py delete mode 100755 cci_venv/bin/rst2odt_prepstyles.py delete mode 100755 cci_venv/bin/rst2pseudoxml.py delete mode 100755 cci_venv/bin/rst2s5.py delete mode 100755 cci_venv/bin/rst2xetex.py delete mode 100755 cci_venv/bin/rst2xml.py delete mode 100755 cci_venv/bin/rstpep2html.py delete mode 100755 cci_venv/bin/snowbench delete mode 100755 cci_venv/bin/snowfakery delete mode 100755 cci_venv/bin/sphinx-apidoc delete mode 100755 cci_venv/bin/sphinx-autogen delete mode 100755 cci_venv/bin/sphinx-build delete mode 100755 cci_venv/bin/sphinx-quickstart delete mode 100755 cci_venv/bin/tox delete mode 100755 cci_venv/bin/virtualenv delete mode 100755 cci_venv/bin/wheel delete mode 100644 cci_venv/include/site/python3.9/greenlet/greenlet.h delete mode 100644 cci_venv/pyvenv.cfg delete mode 100644 cci_venv/requirements/dev.txt delete mode 100644 cci_venv/requirements/prod.txt diff --git a/cci_venv/bin/Activate.ps1 b/cci_venv/bin/Activate.ps1 deleted file mode 100644 index 2fb3852c3c..0000000000 --- a/cci_venv/bin/Activate.ps1 +++ /dev/null @@ -1,241 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/cci_venv/bin/activate b/cci_venv/bin/activate deleted file mode 100644 index 21c2b8d393..0000000000 --- a/cci_venv/bin/activate +++ /dev/null @@ -1,66 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # This should detect bash and zsh, which have a hash command that must - # be called to get it to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null - fi - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV="/Users/l.ramireddy/Downloads/CumulusCI/cci_venv" -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/bin:$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1="(cci_venv) ${PS1:-}" - export PS1 -fi - -# This should detect bash and zsh, which have a hash command that must -# be called to get it to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null -fi diff --git a/cci_venv/bin/activate.csh b/cci_venv/bin/activate.csh deleted file mode 100644 index b9f287280d..0000000000 --- a/cci_venv/bin/activate.csh +++ /dev/null @@ -1,25 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. -# Created by Davide Di Blasi . -# Ported to Python 3.3 venv by Andrew Svetlov - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV "/Users/l.ramireddy/Downloads/CumulusCI/cci_venv" - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/bin:$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - set prompt = "(cci_venv) $prompt" -endif - -alias pydoc python -m pydoc - -rehash diff --git a/cci_venv/bin/activate.fish b/cci_venv/bin/activate.fish deleted file mode 100644 index 0725b5f656..0000000000 --- a/cci_venv/bin/activate.fish +++ /dev/null @@ -1,64 +0,0 @@ -# This file must be used with "source /bin/activate.fish" *from fish* -# (https://fishshell.com/); you cannot run it directly. - -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - functions -e fish_prompt - set -e _OLD_FISH_PROMPT_OVERRIDE - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - - set -e VIRTUAL_ENV - if test "$argv[1]" != "nondestructive" - # Self-destruct! - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV "/Users/l.ramireddy/Downloads/CumulusCI/cci_venv" - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/bin" $PATH - -# Unset PYTHONHOME if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # Save the current fish_prompt function as the function _old_fish_prompt. - functions -c fish_prompt _old_fish_prompt - - # With the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command. - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. - printf "%s%s%s" (set_color 4B8BBE) "(cci_venv) " (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . - # Output the original/"old" prompt. - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" -end diff --git a/cci_venv/bin/black b/cci_venv/bin/black deleted file mode 100755 index 8fddae98b0..0000000000 --- a/cci_venv/bin/black +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from black import patched_main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(patched_main()) diff --git a/cci_venv/bin/blackd b/cci_venv/bin/blackd deleted file mode 100755 index 452a9818c4..0000000000 --- a/cci_venv/bin/blackd +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from blackd import patched_main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(patched_main()) diff --git a/cci_venv/bin/cci b/cci_venv/bin/cci deleted file mode 100755 index 7e3ec88284..0000000000 --- a/cci_venv/bin/cci +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from cumulusci.cli.cci import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/chardetect b/cci_venv/bin/chardetect deleted file mode 100755 index 3e18cdf729..0000000000 --- a/cci_venv/bin/chardetect +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from chardet.cli.chardetect import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/coverage b/cci_venv/bin/coverage deleted file mode 100755 index 3ec554f4f8..0000000000 --- a/cci_venv/bin/coverage +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from coverage.cmdline import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/coverage-3.9 b/cci_venv/bin/coverage-3.9 deleted file mode 100755 index 3ec554f4f8..0000000000 --- a/cci_venv/bin/coverage-3.9 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from coverage.cmdline import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/coverage3 b/cci_venv/bin/coverage3 deleted file mode 100755 index 3ec554f4f8..0000000000 --- a/cci_venv/bin/coverage3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from coverage.cmdline import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/faker b/cci_venv/bin/faker deleted file mode 100755 index f4fef2c5bc..0000000000 --- a/cci_venv/bin/faker +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from faker.cli import execute_from_command_line -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(execute_from_command_line()) diff --git a/cci_venv/bin/flake8 b/cci_venv/bin/flake8 deleted file mode 100755 index 28bccc742c..0000000000 --- a/cci_venv/bin/flake8 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from flake8.main.cli import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/identify-cli b/cci_venv/bin/identify-cli deleted file mode 100755 index a15a809bc2..0000000000 --- a/cci_venv/bin/identify-cli +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from identify.cli import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/isort b/cci_venv/bin/isort deleted file mode 100755 index 63eae32e31..0000000000 --- a/cci_venv/bin/isort +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from isort.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/isort-identify-imports b/cci_venv/bin/isort-identify-imports deleted file mode 100755 index b0d648fb3f..0000000000 --- a/cci_venv/bin/isort-identify-imports +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from isort.main import identify_imports_main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(identify_imports_main()) diff --git a/cci_venv/bin/jsonschema b/cci_venv/bin/jsonschema deleted file mode 100755 index 7e071f8116..0000000000 --- a/cci_venv/bin/jsonschema +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from jsonschema.cli import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/keyring b/cci_venv/bin/keyring deleted file mode 100755 index 0e1bf44fed..0000000000 --- a/cci_venv/bin/keyring +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from keyring.cli import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/libdoc b/cci_venv/bin/libdoc deleted file mode 100755 index e5ed24b522..0000000000 --- a/cci_venv/bin/libdoc +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from robot.libdoc import libdoc_cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(libdoc_cli()) diff --git a/cci_venv/bin/markdown-it b/cci_venv/bin/markdown-it deleted file mode 100755 index f5d45d06be..0000000000 --- a/cci_venv/bin/markdown-it +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from markdown_it.cli.parse import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/myst-anchors b/cci_venv/bin/myst-anchors deleted file mode 100755 index 77e53cbd7b..0000000000 --- a/cci_venv/bin/myst-anchors +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from myst_parser.cli import print_anchors -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(print_anchors()) diff --git a/cci_venv/bin/myst-docutils-html b/cci_venv/bin/myst-docutils-html deleted file mode 100755 index 1d8127474e..0000000000 --- a/cci_venv/bin/myst-docutils-html +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from myst_parser.parsers.docutils_ import cli_html -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_html()) diff --git a/cci_venv/bin/myst-docutils-html5 b/cci_venv/bin/myst-docutils-html5 deleted file mode 100755 index b08822ba1d..0000000000 --- a/cci_venv/bin/myst-docutils-html5 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from myst_parser.parsers.docutils_ import cli_html5 -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_html5()) diff --git a/cci_venv/bin/myst-docutils-latex b/cci_venv/bin/myst-docutils-latex deleted file mode 100755 index f694b85aff..0000000000 --- a/cci_venv/bin/myst-docutils-latex +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from myst_parser.parsers.docutils_ import cli_latex -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_latex()) diff --git a/cci_venv/bin/myst-docutils-pseudoxml b/cci_venv/bin/myst-docutils-pseudoxml deleted file mode 100755 index 32112a5931..0000000000 --- a/cci_venv/bin/myst-docutils-pseudoxml +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from myst_parser.parsers.docutils_ import cli_pseudoxml -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_pseudoxml()) diff --git a/cci_venv/bin/myst-docutils-xml b/cci_venv/bin/myst-docutils-xml deleted file mode 100755 index b2ba7b6267..0000000000 --- a/cci_venv/bin/myst-docutils-xml +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from myst_parser.parsers.docutils_ import cli_xml -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_xml()) diff --git a/cci_venv/bin/natsort b/cci_venv/bin/natsort deleted file mode 100755 index 6f4b97538a..0000000000 --- a/cci_venv/bin/natsort +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from natsort.__main__ import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/nodeenv b/cci_venv/bin/nodeenv deleted file mode 100755 index 1a322f2f46..0000000000 --- a/cci_venv/bin/nodeenv +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from nodeenv import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/normalizer b/cci_venv/bin/normalizer deleted file mode 100755 index 84beb4cfa6..0000000000 --- a/cci_venv/bin/normalizer +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from charset_normalizer.cli.normalizer import cli_detect -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_detect()) diff --git a/cci_venv/bin/pabot b/cci_venv/bin/pabot deleted file mode 100755 index 1c422a1396..0000000000 --- a/cci_venv/bin/pabot +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pabot.pabot import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pip b/cci_venv/bin/pip deleted file mode 100755 index e6124a1a8b..0000000000 --- a/cci_venv/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pip-compile b/cci_venv/bin/pip-compile deleted file mode 100755 index 93a7025865..0000000000 --- a/cci_venv/bin/pip-compile +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from piptools.scripts.compile import cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli()) diff --git a/cci_venv/bin/pip-sync b/cci_venv/bin/pip-sync deleted file mode 100755 index d2aab886e6..0000000000 --- a/cci_venv/bin/pip-sync +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from piptools.scripts.sync import cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli()) diff --git a/cci_venv/bin/pip3 b/cci_venv/bin/pip3 deleted file mode 100755 index e6124a1a8b..0000000000 --- a/cci_venv/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pip3.11 b/cci_venv/bin/pip3.11 deleted file mode 100755 index e6124a1a8b..0000000000 --- a/cci_venv/bin/pip3.11 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pip3.9 b/cci_venv/bin/pip3.9 deleted file mode 100755 index e6124a1a8b..0000000000 --- a/cci_venv/bin/pip3.9 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pre-commit b/cci_venv/bin/pre-commit deleted file mode 100755 index 49e634db94..0000000000 --- a/cci_venv/bin/pre-commit +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pre_commit.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/py.test b/cci_venv/bin/py.test deleted file mode 100755 index 89d65fe7d7..0000000000 --- a/cci_venv/bin/py.test +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pytest import console_main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(console_main()) diff --git a/cci_venv/bin/pybabel b/cci_venv/bin/pybabel deleted file mode 100755 index c8556b4286..0000000000 --- a/cci_venv/bin/pybabel +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from babel.messages.frontend import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pycodestyle b/cci_venv/bin/pycodestyle deleted file mode 100755 index 78806a67b0..0000000000 --- a/cci_venv/bin/pycodestyle +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pycodestyle import _main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(_main()) diff --git a/cci_venv/bin/pyflakes b/cci_venv/bin/pyflakes deleted file mode 100755 index 411cd3b657..0000000000 --- a/cci_venv/bin/pyflakes +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pyflakes.api import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pygmentize b/cci_venv/bin/pygmentize deleted file mode 100755 index acd74380f3..0000000000 --- a/cci_venv/bin/pygmentize +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pygments.cmdline import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/pyproject-build b/cci_venv/bin/pyproject-build deleted file mode 100755 index a6ce606f38..0000000000 --- a/cci_venv/bin/pyproject-build +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from build.__main__ import entrypoint -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(entrypoint()) diff --git a/cci_venv/bin/pytest b/cci_venv/bin/pytest deleted file mode 100755 index 89d65fe7d7..0000000000 --- a/cci_venv/bin/pytest +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from pytest import console_main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(console_main()) diff --git a/cci_venv/bin/python b/cci_venv/bin/python deleted file mode 120000 index b8a0adbbb9..0000000000 --- a/cci_venv/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/cci_venv/bin/python3 b/cci_venv/bin/python3 deleted file mode 120000 index f25545feec..0000000000 --- a/cci_venv/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -/Library/Developer/CommandLineTools/usr/bin/python3 \ No newline at end of file diff --git a/cci_venv/bin/python3.9 b/cci_venv/bin/python3.9 deleted file mode 120000 index b8a0adbbb9..0000000000 --- a/cci_venv/bin/python3.9 +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/cci_venv/bin/rebot b/cci_venv/bin/rebot deleted file mode 100755 index 0f81b389f7..0000000000 --- a/cci_venv/bin/rebot +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from robot.rebot import rebot_cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(rebot_cli()) diff --git a/cci_venv/bin/rflint b/cci_venv/bin/rflint deleted file mode 100755 index e649a3e7b1..0000000000 --- a/cci_venv/bin/rflint +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from rflint.__main__ import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/robot b/cci_venv/bin/robot deleted file mode 100755 index d8ca923a58..0000000000 --- a/cci_venv/bin/robot +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from robot.run import run_cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(run_cli()) diff --git a/cci_venv/bin/rst2ansi b/cci_venv/bin/rst2ansi deleted file mode 100755 index 631cb26005..0000000000 --- a/cci_venv/bin/rst2ansi +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/python - -import sys -from rst2ansi import rst2ansi -import argparse -import io - -parser = argparse.ArgumentParser(description='Prints a reStructuredText input in an ansi-decorated format suitable for console output.') -parser.add_argument('file', type=str, nargs='?', help='A path to the file to open') - -args = parser.parse_args() - -def process_file(f): - out = rst2ansi(f.read()) - if out: - try: - print(out) - except UnicodeEncodeError: - print(out.encode('ascii', errors='backslashreplace').decode('ascii')) - -if args.file: - with io.open(args.file, 'rb') as f: - process_file(f) -else: - process_file(sys.stdin) - diff --git a/cci_venv/bin/rst2html.py b/cci_venv/bin/rst2html.py deleted file mode 100755 index 4deeb673e1..0000000000 --- a/cci_venv/bin/rst2html.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2html.py 4564 2006-05-21 20:44:42Z wiemann $ -# Author: David Goodger -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing HTML. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description - - -description = ('Generates (X)HTML documents from standalone reStructuredText ' - 'sources. ' + default_description) - -publish_cmdline(writer_name='html', description=description) diff --git a/cci_venv/bin/rst2html4.py b/cci_venv/bin/rst2html4.py deleted file mode 100755 index 26b7083fdf..0000000000 --- a/cci_venv/bin/rst2html4.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2html4.py 7994 2016-12-10 17:41:45Z milde $ -# Author: David Goodger -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing (X)HTML. - -The output conforms to XHTML 1.0 transitional -and almost to HTML 4.01 transitional (except for closing empty tags). -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description - - -description = ('Generates (X)HTML documents from standalone reStructuredText ' - 'sources. ' + default_description) - -publish_cmdline(writer_name='html4', description=description) diff --git a/cci_venv/bin/rst2html5.py b/cci_venv/bin/rst2html5.py deleted file mode 100755 index 07bb91cc24..0000000000 --- a/cci_venv/bin/rst2html5.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf8 -*- -# :Copyright: © 2015 Günter Milde. -# :License: Released under the terms of the `2-Clause BSD license`_, in short: -# -# Copying and distribution of this file, with or without modification, -# are permitted in any medium without royalty provided the copyright -# notice and this notice are preserved. -# This file is offered as-is, without any warranty. -# -# .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause -# -# Revision: $Revision: 8410 $ -# Date: $Date: 2019-11-04 22:14:43 +0100 (Mo, 04. Nov 2019) $ - -""" -A minimal front end to the Docutils Publisher, producing HTML 5 documents. - -The output also conforms to XHTML 1.0 transitional -(except for the doctype declaration). -""" - -try: - import locale # module missing in Jython - locale.setlocale(locale.LC_ALL, '') -except locale.Error: - pass - -from docutils.core import publish_cmdline, default_description - -description = (u'Generates HTML 5 documents from standalone ' - u'reStructuredText sources ' - + default_description) - -publish_cmdline(writer_name='html5', description=description) diff --git a/cci_venv/bin/rst2latex.py b/cci_venv/bin/rst2latex.py deleted file mode 100755 index 9dcd011a95..0000000000 --- a/cci_venv/bin/rst2latex.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2latex.py 5905 2009-04-16 12:04:49Z milde $ -# Author: David Goodger -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing LaTeX. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline - -description = ('Generates LaTeX documents from standalone reStructuredText ' - 'sources. ' - 'Reads from (default is stdin) and writes to ' - ' (default is stdout). See ' - ' for ' - 'the full reference.') - -publish_cmdline(writer_name='latex', description=description) diff --git a/cci_venv/bin/rst2man.py b/cci_venv/bin/rst2man.py deleted file mode 100755 index 3c40200330..0000000000 --- a/cci_venv/bin/rst2man.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# Author: -# Contact: grubert@users.sf.net -# Copyright: This module has been placed in the public domain. - -""" -man.py -====== - -This module provides a simple command line interface that uses the -man page writer to output from ReStructuredText source. -""" - -import locale -try: - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description -from docutils.writers import manpage - -description = ("Generates plain unix manual documents. " + default_description) - -publish_cmdline(writer=manpage.Writer(), description=description) diff --git a/cci_venv/bin/rst2odt.py b/cci_venv/bin/rst2odt.py deleted file mode 100755 index 0be802f64b..0000000000 --- a/cci_venv/bin/rst2odt.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2odt.py 5839 2009-01-07 19:09:28Z dkuhlman $ -# Author: Dave Kuhlman -# Copyright: This module has been placed in the public domain. - -""" -A front end to the Docutils Publisher, producing OpenOffice documents. -""" - -import sys -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline_to_binary, default_description -from docutils.writers.odf_odt import Writer, Reader - - -description = ('Generates OpenDocument/OpenOffice/ODF documents from ' - 'standalone reStructuredText sources. ' + default_description) - - -writer = Writer() -reader = Reader() -output = publish_cmdline_to_binary(reader=reader, writer=writer, - description=description) - diff --git a/cci_venv/bin/rst2odt_prepstyles.py b/cci_venv/bin/rst2odt_prepstyles.py deleted file mode 100755 index 727e87f91d..0000000000 --- a/cci_venv/bin/rst2odt_prepstyles.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2odt_prepstyles.py 8346 2019-08-26 12:11:32Z milde $ -# Author: Dave Kuhlman -# Copyright: This module has been placed in the public domain. - -""" -Fix a word-processor-generated styles.odt for odtwriter use: Drop page size -specifications from styles.xml in STYLE_FILE.odt. -""" - -# Author: Michael Schutte - -from __future__ import print_function - -from lxml import etree -import sys -import zipfile -from tempfile import mkstemp -import shutil -import os - -NAMESPACES = { - "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", - "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" -} - - -def prepstyle(filename): - - zin = zipfile.ZipFile(filename) - styles = zin.read("styles.xml") - - root = etree.fromstring(styles) - for el in root.xpath("//style:page-layout-properties", - namespaces=NAMESPACES): - for attr in el.attrib: - if attr.startswith("{%s}" % NAMESPACES["fo"]): - del el.attrib[attr] - - tempname = mkstemp() - zout = zipfile.ZipFile(os.fdopen(tempname[0], "w"), "w", - zipfile.ZIP_DEFLATED) - - for item in zin.infolist(): - if item.filename == "styles.xml": - zout.writestr(item, etree.tostring(root)) - else: - zout.writestr(item, zin.read(item.filename)) - - zout.close() - zin.close() - shutil.move(tempname[1], filename) - - -def main(): - args = sys.argv[1:] - if len(args) != 1: - print(__doc__, file=sys.stderr) - print("Usage: %s STYLE_FILE.odt\n" % sys.argv[0], file=sys.stderr) - sys.exit(1) - filename = args[0] - prepstyle(filename) - - -if __name__ == '__main__': - main() diff --git a/cci_venv/bin/rst2pseudoxml.py b/cci_venv/bin/rst2pseudoxml.py deleted file mode 100755 index 717bf2e1ea..0000000000 --- a/cci_venv/bin/rst2pseudoxml.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2pseudoxml.py 4564 2006-05-21 20:44:42Z wiemann $ -# Author: David Goodger -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing pseudo-XML. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description - - -description = ('Generates pseudo-XML from standalone reStructuredText ' - 'sources (for testing purposes). ' + default_description) - -publish_cmdline(description=description) diff --git a/cci_venv/bin/rst2s5.py b/cci_venv/bin/rst2s5.py deleted file mode 100755 index 196249d292..0000000000 --- a/cci_venv/bin/rst2s5.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2s5.py 4564 2006-05-21 20:44:42Z wiemann $ -# Author: Chris Liechti -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing HTML slides using -the S5 template system. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description - - -description = ('Generates S5 (X)HTML slideshow documents from standalone ' - 'reStructuredText sources. ' + default_description) - -publish_cmdline(writer_name='s5', description=description) diff --git a/cci_venv/bin/rst2xetex.py b/cci_venv/bin/rst2xetex.py deleted file mode 100755 index 5c50c3f7b8..0000000000 --- a/cci_venv/bin/rst2xetex.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2xetex.py 7847 2015-03-17 17:30:47Z milde $ -# Author: Guenter Milde -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing Lua/XeLaTeX code. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline - -description = ('Generates LaTeX documents from standalone reStructuredText ' - 'sources for compilation with the Unicode-aware TeX variants ' - 'XeLaTeX or LuaLaTeX. ' - 'Reads from (default is stdin) and writes to ' - ' (default is stdout). See ' - ' for ' - 'the full reference.') - -publish_cmdline(writer_name='xetex', description=description) diff --git a/cci_venv/bin/rst2xml.py b/cci_venv/bin/rst2xml.py deleted file mode 100755 index db68e2f214..0000000000 --- a/cci_venv/bin/rst2xml.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rst2xml.py 4564 2006-05-21 20:44:42Z wiemann $ -# Author: David Goodger -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing Docutils XML. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description - - -description = ('Generates Docutils-native XML from standalone ' - 'reStructuredText sources. ' + default_description) - -publish_cmdline(writer_name='xml', description=description) diff --git a/cci_venv/bin/rstpep2html.py b/cci_venv/bin/rstpep2html.py deleted file mode 100755 index 4fb4508aaa..0000000000 --- a/cci_venv/bin/rstpep2html.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python - -# $Id: rstpep2html.py 4564 2006-05-21 20:44:42Z wiemann $ -# Author: David Goodger -# Copyright: This module has been placed in the public domain. - -""" -A minimal front end to the Docutils Publisher, producing HTML from PEP -(Python Enhancement Proposal) documents. -""" - -try: - import locale - locale.setlocale(locale.LC_ALL, '') -except: - pass - -from docutils.core import publish_cmdline, default_description - - -description = ('Generates (X)HTML from reStructuredText-format PEP files. ' - + default_description) - -publish_cmdline(reader_name='pep', writer_name='pep_html', - description=description) diff --git a/cci_venv/bin/snowbench b/cci_venv/bin/snowbench deleted file mode 100755 index e75bb04614..0000000000 --- a/cci_venv/bin/snowbench +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from snowfakery.tools.snowbench import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/snowfakery b/cci_venv/bin/snowfakery deleted file mode 100755 index 8bfe15089b..0000000000 --- a/cci_venv/bin/snowfakery +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from snowfakery.cli import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/sphinx-apidoc b/cci_venv/bin/sphinx-apidoc deleted file mode 100755 index fd351c9e85..0000000000 --- a/cci_venv/bin/sphinx-apidoc +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from sphinx.ext.apidoc import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/sphinx-autogen b/cci_venv/bin/sphinx-autogen deleted file mode 100755 index 4f605a06bb..0000000000 --- a/cci_venv/bin/sphinx-autogen +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from sphinx.ext.autosummary.generate import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/sphinx-build b/cci_venv/bin/sphinx-build deleted file mode 100755 index a4d9d78528..0000000000 --- a/cci_venv/bin/sphinx-build +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from sphinx.cmd.build import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/sphinx-quickstart b/cci_venv/bin/sphinx-quickstart deleted file mode 100755 index 8b8f570daa..0000000000 --- a/cci_venv/bin/sphinx-quickstart +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from sphinx.cmd.quickstart import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/bin/tox b/cci_venv/bin/tox deleted file mode 100755 index e288d3a32d..0000000000 --- a/cci_venv/bin/tox +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from tox.run import run -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(run()) diff --git a/cci_venv/bin/virtualenv b/cci_venv/bin/virtualenv deleted file mode 100755 index e22135d441..0000000000 --- a/cci_venv/bin/virtualenv +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from virtualenv.__main__ import run_with_catch -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(run_with_catch()) diff --git a/cci_venv/bin/wheel b/cci_venv/bin/wheel deleted file mode 100755 index ac53361d94..0000000000 --- a/cci_venv/bin/wheel +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/l.ramireddy/Downloads/CumulusCI/cci_venv/bin/python -# -*- coding: utf-8 -*- -import re -import sys -from wheel.cli import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/cci_venv/include/site/python3.9/greenlet/greenlet.h b/cci_venv/include/site/python3.9/greenlet/greenlet.h deleted file mode 100644 index d02a16e434..0000000000 --- a/cci_venv/include/site/python3.9/greenlet/greenlet.h +++ /dev/null @@ -1,164 +0,0 @@ -/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ - -/* Greenlet object interface */ - -#ifndef Py_GREENLETOBJECT_H -#define Py_GREENLETOBJECT_H - - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* This is deprecated and undocumented. It does not change. */ -#define GREENLET_VERSION "1.0.0" - -#ifndef GREENLET_MODULE -#define implementation_ptr_t void* -#endif - -typedef struct _greenlet { - PyObject_HEAD - PyObject* weakreflist; - PyObject* dict; - implementation_ptr_t pimpl; -} PyGreenlet; - -#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) - - -/* C API functions */ - -/* Total number of symbols that are exported */ -#define PyGreenlet_API_pointers 12 - -#define PyGreenlet_Type_NUM 0 -#define PyExc_GreenletError_NUM 1 -#define PyExc_GreenletExit_NUM 2 - -#define PyGreenlet_New_NUM 3 -#define PyGreenlet_GetCurrent_NUM 4 -#define PyGreenlet_Throw_NUM 5 -#define PyGreenlet_Switch_NUM 6 -#define PyGreenlet_SetParent_NUM 7 - -#define PyGreenlet_MAIN_NUM 8 -#define PyGreenlet_STARTED_NUM 9 -#define PyGreenlet_ACTIVE_NUM 10 -#define PyGreenlet_GET_PARENT_NUM 11 - -#ifndef GREENLET_MODULE -/* This section is used by modules that uses the greenlet C API */ -static void** _PyGreenlet_API = NULL; - -# define PyGreenlet_Type \ - (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) - -# define PyExc_GreenletError \ - ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) - -# define PyExc_GreenletExit \ - ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) - -/* - * PyGreenlet_New(PyObject *args) - * - * greenlet.greenlet(run, parent=None) - */ -# define PyGreenlet_New \ - (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ - _PyGreenlet_API[PyGreenlet_New_NUM]) - -/* - * PyGreenlet_GetCurrent(void) - * - * greenlet.getcurrent() - */ -# define PyGreenlet_GetCurrent \ - (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) - -/* - * PyGreenlet_Throw( - * PyGreenlet *greenlet, - * PyObject *typ, - * PyObject *val, - * PyObject *tb) - * - * g.throw(...) - */ -# define PyGreenlet_Throw \ - (*(PyObject * (*)(PyGreenlet * self, \ - PyObject * typ, \ - PyObject * val, \ - PyObject * tb)) \ - _PyGreenlet_API[PyGreenlet_Throw_NUM]) - -/* - * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) - * - * g.switch(*args, **kwargs) - */ -# define PyGreenlet_Switch \ - (*(PyObject * \ - (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ - _PyGreenlet_API[PyGreenlet_Switch_NUM]) - -/* - * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) - * - * g.parent = new_parent - */ -# define PyGreenlet_SetParent \ - (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ - _PyGreenlet_API[PyGreenlet_SetParent_NUM]) - -/* - * PyGreenlet_GetParent(PyObject* greenlet) - * - * return greenlet.parent; - * - * This could return NULL even if there is no exception active. - * If it does not return NULL, you are responsible for decrementing the - * reference count. - */ -# define PyGreenlet_GetParent \ - (*(PyGreenlet* (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) - -/* - * deprecated, undocumented alias. - */ -# define PyGreenlet_GET_PARENT PyGreenlet_GetParent - -# define PyGreenlet_MAIN \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_MAIN_NUM]) - -# define PyGreenlet_STARTED \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_STARTED_NUM]) - -# define PyGreenlet_ACTIVE \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) - - - - -/* Macro that imports greenlet and initializes C API */ -/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we - keep the older definition to be sure older code that might have a copy of - the header still works. */ -# define PyGreenlet_Import() \ - { \ - _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ - } - -#endif /* GREENLET_MODULE */ - -#ifdef __cplusplus -} -#endif -#endif /* !Py_GREENLETOBJECT_H */ diff --git a/cci_venv/pyvenv.cfg b/cci_venv/pyvenv.cfg deleted file mode 100644 index 4760c1ffc4..0000000000 --- a/cci_venv/pyvenv.cfg +++ /dev/null @@ -1,3 +0,0 @@ -home = /Library/Developer/CommandLineTools/usr/bin -include-system-site-packages = false -version = 3.9.6 diff --git a/cci_venv/requirements/dev.txt b/cci_venv/requirements/dev.txt deleted file mode 100644 index 6af1da5af7..0000000000 --- a/cci_venv/requirements/dev.txt +++ /dev/null @@ -1,229 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --allow-unsafe requirements/dev.in -# -attrs==22.2.0 - # via - # jsonschema - # pytest -black==23.1.0 - # via -r requirements/dev.in -cachetools==5.3.0 - # via tox -certifi==2022.12.7 - # via - # -r requirements/prod.txt - # requests -cfgv==3.3.1 - # via pre-commit -chardet==5.1.0 - # via - # diff-cover - # tox -charset-normalizer==3.0.1 - # via - # -r requirements/prod.txt - # requests -click==8.1.3 - # via - # -r requirements/prod.txt - # black - # mkdocs -colorama==0.4.6 - # via tox -coverage[toml]==6.5.0 - # via - # -r requirements/dev.in - # coveralls - # pytest-cov -coveralls==3.3.1 - # via -r requirements/dev.in -diff-cover==7.4.0 - # via -r requirements/dev.in -distlib==0.3.6 - # via virtualenv -docopt==0.6.2 - # via coveralls -exceptiongroup==1.1.0 - # via pytest -faker==17.0.0 - # via - # -r requirements/prod.txt - # faker-microservice -faker-microservice==2.0.0 - # via -r requirements/dev.in -filelock==3.9.0 - # via - # tox - # virtualenv -ghp-import==2.1.0 - # via mkdocs -greenlet==2.0.2 - # via - # -r requirements/prod.txt - # sqlalchemy -gvgen==1.0 - # via -r requirements/prod.txt -identify==2.5.18 - # via pre-commit -idna==3.4 - # via - # -r requirements/prod.txt - # requests - # yarl -importlib-metadata==6.0.0 - # via - # markdown - # mkdocs -iniconfig==2.0.0 - # via pytest -jinja2==3.1.2 - # via - # -r requirements/prod.txt - # diff-cover - # mkdocs -jsonschema==4.17.3 - # via -r requirements/dev.in -markdown==3.4.1 - # via mkdocs -markupsafe==2.1.2 - # via - # -r requirements/prod.txt - # jinja2 -mergedeep==1.3.4 - # via mkdocs -mkdocs==1.2.4 - # via - # -r requirements/dev.in - # mkdocs-exclude-search -mkdocs-exclude-search==0.6.5 - # via -r requirements/dev.in -multidict==6.0.4 - # via yarl -mypy-extensions==1.0.0 - # via black -nodeenv==1.7.0 - # via - # pre-commit - # pyright -packaging==23.0 - # via - # black - # mkdocs - # pyproject-api - # pytest - # tox -pathspec==0.11.0 - # via black -platformdirs==3.0.0 - # via - # black - # tox - # virtualenv -pluggy==1.0.0 - # via - # diff-cover - # pytest - # tox -pre-commit==3.0.4 - # via -r requirements/dev.in -pydantic==1.10.5 - # via -r requirements/prod.txt -pygments==2.14.0 - # via diff-cover -pyproject-api==1.5.0 - # via tox -pyright==1.1.294 - # via -r requirements/dev.in -pyrsistent==0.19.3 - # via jsonschema -pytest==7.2.1 - # via - # -r requirements/dev.in - # pytest-cov - # pytest-vcr -pytest-cov==4.0.0 - # via -r requirements/dev.in -pytest-vcr==1.0.2 - # via -r requirements/dev.in -python-baseconv==1.2.2 - # via -r requirements/prod.txt -python-dateutil==2.8.2 - # via - # -r requirements/prod.txt - # faker - # ghp-import -pyyaml==6.0 - # via - # -r requirements/prod.txt - # mkdocs - # pre-commit - # pyyaml-env-tag - # vcrpy -pyyaml-env-tag==0.1 - # via mkdocs -requests==2.28.2 - # via - # -r requirements/prod.txt - # coveralls - # responses -responses==0.22.0 - # via -r requirements/dev.in -six==1.16.0 - # via - # -r requirements/prod.txt - # python-dateutil - # vcrpy -sqlalchemy==1.4.46 - # via -r requirements/prod.txt -toml==0.10.2 - # via responses -tomli==2.0.1 - # via - # black - # coverage - # pyproject-api - # pytest - # tox -tox==4.4.5 - # via - # -r requirements/dev.in - # tox-gh-actions -tox-gh-actions==3.0.0 - # via -r requirements/dev.in -typeguard==2.13.3 - # via -r requirements/dev.in -types-toml==0.10.8.4 - # via responses -typing-extensions==4.5.0 - # via - # -r requirements/prod.txt - # black - # pydantic -urllib3==1.26.14 - # via - # -r requirements/prod.txt - # requests - # responses -vcrpy==4.2.1 - # via - # -r requirements/dev.in - # pytest-vcr -virtualenv==20.19.0 - # via - # pre-commit - # tox -watchdog==2.2.1 - # via mkdocs -wrapt==1.14.1 - # via vcrpy -yarl==1.8.2 - # via vcrpy -zipp==3.13.0 - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -setuptools==67.3.2 - # via nodeenv diff --git a/cci_venv/requirements/prod.txt b/cci_venv/requirements/prod.txt deleted file mode 100644 index affd25463e..0000000000 --- a/cci_venv/requirements/prod.txt +++ /dev/null @@ -1,44 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --allow-unsafe requirements/prod.in -# -certifi==2022.12.7 - # via requests -charset-normalizer==3.0.1 - # via requests -click==8.1.3 - # via -r requirements/prod.in -faker==17.0.0 - # via -r requirements/prod.in -greenlet==2.0.2 - # via sqlalchemy -gvgen==1.0 - # via -r requirements/prod.in -idna==3.4 - # via requests -jinja2==3.1.2 - # via -r requirements/prod.in -markupsafe==2.1.2 - # via jinja2 -pydantic==1.10.5 - # via -r requirements/prod.in -python-baseconv==1.2.2 - # via -r requirements/prod.in -python-dateutil==2.8.2 - # via - # -r requirements/prod.in - # faker -pyyaml==6.0 - # via -r requirements/prod.in -requests==2.28.2 - # via -r requirements/prod.in -six==1.16.0 - # via python-dateutil -sqlalchemy==1.4.46 - # via -r requirements/prod.in -typing-extensions==4.5.0 - # via pydantic -urllib3==1.26.14 - # via requests From ec8716e02cb3a6c68f59714bdf9856b17a752c4b Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 5 Oct 2023 13:01:10 +0530 Subject: [PATCH 42/98] clean-up --- .gitignore | 1 + cumulusci/salesforce_api/metadata.py | 30 +++++++++------- .../tests/metadata_test_strings.py | 5 +++ .../salesforce_api/tests/test_metadata.py | 35 +++++++++++++++++++ .../tasks/salesforce/RetrieveMetadataTypes.py | 22 +++++------- 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index d91ee2ee9b..2a85104501 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ docs/api/ docs/_build/ github_release_notes.html tmp +cci_venv/ # Distribution / packaging *.egg diff --git a/cumulusci/salesforce_api/metadata.py b/cumulusci/salesforce_api/metadata.py index c95fd29783..72d13f30fc 100644 --- a/cumulusci/salesforce_api/metadata.py +++ b/cumulusci/salesforce_api/metadata.py @@ -685,9 +685,7 @@ class ApiListMetadataTypes(BaseMetadataApiCall): soap_envelope_start = soap_envelopes.METADATA_TYPES soap_action_start = "describemetadatatypes" - def __init__( - self, task, as_of_version=None - ): + def __init__(self, task, as_of_version=None): super(ApiListMetadataTypes, self).__init__(task) self.metadata_types = [] self.as_of_version = ( @@ -698,20 +696,26 @@ def __init__( self.api_version = self.as_of_version def _build_envelope_start(self): - + return self.soap_envelope_start.format( as_of_version=self.as_of_version, ) def _process_response(self, response): - self.metadata_types=[] - temp=parseString(response).getElementsByTagName("metadataObjects") - - for metadataobject in temp: - self.metadata_types.append(self._get_element_value(metadataobject, "xmlName")) - child_elements = metadataobject.getElementsByTagName("childXmlNames") - child_xml_names = [element.firstChild.nodeValue for element in child_elements] - self.metadata_types+=child_xml_names + self.metadata_types = [] + response = response.content.decode("utf-8") + metaobjects = parseString(response).getElementsByTagName("metadataObjects") + for metadataobject in metaobjects: + self.metadata_types.append( + self._get_element_value(metadataobject, "xmlName") + ) + child_elements = metadataobject.getElementsByTagName("childXmlNames") + child_xml_names = [ + element.firstChild.nodeValue for element in child_elements + ] + self.metadata_types += child_xml_names + self.metadata_types.sort() + self.status = "Done" + self.task.logger.info(self.status) return self.metadata_types - diff --git a/cumulusci/salesforce_api/tests/metadata_test_strings.py b/cumulusci/salesforce_api/tests/metadata_test_strings.py index 8ab145a752..3075648cec 100644 --- a/cumulusci/salesforce_api/tests/metadata_test_strings.py +++ b/cumulusci/salesforce_api/tests/metadata_test_strings.py @@ -19,3 +19,8 @@ status_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {process_id}\n \n \n' deploy_status_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {process_id}\n true\n \n \n' + + +list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {as_of_version}\n \n \n ' + +list_metadata_types_result = 'WorkflowFieldUpdateworkflowsfalsefalseworkflowWorkflowtruefalse' diff --git a/cumulusci/salesforce_api/tests/test_metadata.py b/cumulusci/salesforce_api/tests/test_metadata.py index 7e578a34bb..fa0e0c2767 100644 --- a/cumulusci/salesforce_api/tests/test_metadata.py +++ b/cumulusci/salesforce_api/tests/test_metadata.py @@ -19,6 +19,7 @@ from cumulusci.salesforce_api.metadata import ( ApiDeploy, ApiListMetadata, + ApiListMetadataTypes, ApiRetrieveInstalledPackages, ApiRetrievePackaged, ApiRetrieveUnpackaged, @@ -36,6 +37,8 @@ list_metadata_result, list_metadata_result_bad_val, list_metadata_start_envelope, + list_metadata_types_envelope, + list_metadata_types_result, result_envelope, retrieve_packaged_start_envelope, retrieve_result, @@ -843,6 +846,38 @@ def test_bad_date_somehow(self): api() +class TestApiListMetadataTypes(TestBaseTestMetadataApi): + api_class = ApiListMetadataTypes + envelope_start = list_metadata_types_envelope + + def setup_method(self): + super().setup_method() + self.metadata_types = None + self.api_version = self.project_config.project__package__api_version + + def _response_call_success_result(self, response_result): + return list_metadata_types_result + + def _expected_call_success_result(self, response_result): + metadata_types = ["Workflow", "WorkflowFieldUpdate"] + return metadata_types + + def _create_instance(self, task, api_version=None): + return super()._create_instance(task, api_version) + + @responses.activate + def test_call_success(self): + org_config = { + "instance_url": "https://na12.salesforce.com", + "id": "https://login.salesforce.com/id/00D000000000000ABC/005000000000000ABC", + "access_token": "0123456789", + } + task = self._create_task(org_config=org_config) + api = self._create_instance(task) + if not self.api_class.soap_envelope_start: + api.soap_envelope_start = "{api_version}" + + class TestApiRetrieveUnpackaged(TestBaseTestMetadataApi): maxDiff = None api_class = ApiRetrieveUnpackaged diff --git a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py index aa57ec749a..99f6806a0d 100644 --- a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py +++ b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py @@ -1,17 +1,15 @@ -from typing import Any from cumulusci.salesforce_api.metadata import ApiListMetadataTypes - from cumulusci.tasks.salesforce import BaseRetrieveMetadata -from defusedxml.minidom import parseString + class RetrieveMetadataTypes(BaseRetrieveMetadata): - api_class= ApiListMetadataTypes + api_class = ApiListMetadataTypes task_options = { "api_version": { "description": "Override the API version used to list metadatatypes" }, - } + def _init_options(self, kwargs): super(RetrieveMetadataTypes, self)._init_options(kwargs) if "api_version" not in self.options: @@ -20,16 +18,12 @@ def _init_options(self, kwargs): ] = self.project_config.project__package__api_version def _get_api(self): - return self.api_class( - self, self.options.get("api_version") - ) + return self.api_class(self, self.options.get("api_version")) def _run_task(self): api_object = self._get_api() - root = api_object._get_response().content.decode("utf-8") - self.logger.info(api_object._process_response(root)) - - - - \ No newline at end of file + while api_object.status == "Pending": + api_object() + + self.logger.info("Metadata Types supported by org:\n" + str(api_object())) From d54afc4661ca206a78912a502ce972dcd085fefd Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 5 Oct 2023 17:05:13 +0530 Subject: [PATCH 43/98] tests_added --- cumulusci/salesforce_api/metadata.py | 11 ++++---- cumulusci/salesforce_api/soap_envelopes.py | 4 +-- .../tests/metadata_test_strings.py | 3 ++- .../salesforce_api/tests/test_metadata.py | 6 ++++- .../tests/test_retrievemetadatatypes.py | 26 +++++++++++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py diff --git a/cumulusci/salesforce_api/metadata.py b/cumulusci/salesforce_api/metadata.py index 72d13f30fc..ed82bd380b 100644 --- a/cumulusci/salesforce_api/metadata.py +++ b/cumulusci/salesforce_api/metadata.py @@ -685,20 +685,19 @@ class ApiListMetadataTypes(BaseMetadataApiCall): soap_envelope_start = soap_envelopes.METADATA_TYPES soap_action_start = "describemetadatatypes" - def __init__(self, task, as_of_version=None): + def __init__(self, task, api_version=None): super(ApiListMetadataTypes, self).__init__(task) self.metadata_types = [] - self.as_of_version = ( - as_of_version - if as_of_version + self.api_version = ( + api_version + if api_version else task.project_config.project__package__api_version ) - self.api_version = self.as_of_version def _build_envelope_start(self): return self.soap_envelope_start.format( - as_of_version=self.as_of_version, + api_version=self.api_version, ) def _process_response(self, response): diff --git a/cumulusci/salesforce_api/soap_envelopes.py b/cumulusci/salesforce_api/soap_envelopes.py index 3ccf02c0d0..f92cc8e542 100644 --- a/cumulusci/salesforce_api/soap_envelopes.py +++ b/cumulusci/salesforce_api/soap_envelopes.py @@ -162,7 +162,7 @@ """ -METADATA_TYPES= """ +METADATA_TYPES = """ @@ -171,7 +171,7 @@ - {as_of_version} + {api_version} """ diff --git a/cumulusci/salesforce_api/tests/metadata_test_strings.py b/cumulusci/salesforce_api/tests/metadata_test_strings.py index 3075648cec..c58d660908 100644 --- a/cumulusci/salesforce_api/tests/metadata_test_strings.py +++ b/cumulusci/salesforce_api/tests/metadata_test_strings.py @@ -21,6 +21,7 @@ deploy_status_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {process_id}\n true\n \n \n' -list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {as_of_version}\n \n \n ' +# list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {api_version}\n \n \n' +list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {api_version}\n \n \n' list_metadata_types_result = 'WorkflowFieldUpdateworkflowsfalsefalseworkflowWorkflowtruefalse' diff --git a/cumulusci/salesforce_api/tests/test_metadata.py b/cumulusci/salesforce_api/tests/test_metadata.py index fa0e0c2767..9df439f67e 100644 --- a/cumulusci/salesforce_api/tests/test_metadata.py +++ b/cumulusci/salesforce_api/tests/test_metadata.py @@ -858,7 +858,7 @@ def setup_method(self): def _response_call_success_result(self, response_result): return list_metadata_types_result - def _expected_call_success_result(self, response_result): + def _expected_call_success_result(self, response_result=None): metadata_types = ["Workflow", "WorkflowFieldUpdate"] return metadata_types @@ -876,6 +876,10 @@ def test_call_success(self): api = self._create_instance(task) if not self.api_class.soap_envelope_start: api.soap_envelope_start = "{api_version}" + self._mock_call_mdapi(api, list_metadata_types_result) + + metadata_types = api() + assert metadata_types == self._expected_call_success_result() class TestApiRetrieveUnpackaged(TestBaseTestMetadataApi): diff --git a/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py b/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py new file mode 100644 index 0000000000..c15a01ea0d --- /dev/null +++ b/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py @@ -0,0 +1,26 @@ + + +from unittest import mock + +from cumulusci.tasks.salesforce import RetrieveMetadataTypes +from .util import create_task + +class TestRetrieveMetadataTypes: + def test_run_task(self): + task = create_task(RetrieveMetadataTypes) + task._get_api = mock.Mock() + task() + task._get_api.assert_called_once() + + + def test_run_task_with_apiversion(self): + task = create_task(RetrieveMetadataTypes,{"api_version": 8.0}) + assert task.options.get("api_version")==8.0 + task._get_api() + + + + + + + From f9d47ea2f5f8e25645e84f2eaffef7c0b9d53874 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 5 Oct 2023 17:08:07 +0530 Subject: [PATCH 44/98] tests_added --- .../tests/test_retrievemetadatatypes.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py b/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py index c15a01ea0d..07345aa2ca 100644 --- a/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py +++ b/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py @@ -1,10 +1,10 @@ - - from unittest import mock from cumulusci.tasks.salesforce import RetrieveMetadataTypes + from .util import create_task + class TestRetrieveMetadataTypes: def test_run_task(self): task = create_task(RetrieveMetadataTypes) @@ -12,15 +12,7 @@ def test_run_task(self): task() task._get_api.assert_called_once() - def test_run_task_with_apiversion(self): - task = create_task(RetrieveMetadataTypes,{"api_version": 8.0}) - assert task.options.get("api_version")==8.0 + task = create_task(RetrieveMetadataTypes, {"api_version": 8.0}) + assert task.options.get("api_version") == 8.0 task._get_api() - - - - - - - From 510aefd6abb827b807867680008bff01ba28e528 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 5 Oct 2023 17:10:25 +0530 Subject: [PATCH 45/98] tests_added --- cumulusci/salesforce_api/tests/metadata_test_strings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cumulusci/salesforce_api/tests/metadata_test_strings.py b/cumulusci/salesforce_api/tests/metadata_test_strings.py index c58d660908..4163fecdbb 100644 --- a/cumulusci/salesforce_api/tests/metadata_test_strings.py +++ b/cumulusci/salesforce_api/tests/metadata_test_strings.py @@ -20,8 +20,5 @@ deploy_status_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {process_id}\n true\n \n \n' - -# list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {api_version}\n \n \n' - list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {api_version}\n \n \n' list_metadata_types_result = 'WorkflowFieldUpdateworkflowsfalsefalseworkflowWorkflowtruefalse' From 09c8e88f084e25d28ad06f254aba551a10e658d4 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 5 Oct 2023 17:43:17 +0530 Subject: [PATCH 46/98] Clean Up --- cumulusci/salesforce_api/soap_envelopes.py | 21 ++++++++++--------- .../tests/metadata_test_strings.py | 2 +- .../tasks/salesforce/RetrieveMetadataTypes.py | 4 +++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cumulusci/salesforce_api/soap_envelopes.py b/cumulusci/salesforce_api/soap_envelopes.py index f92cc8e542..2f83e37be6 100644 --- a/cumulusci/salesforce_api/soap_envelopes.py +++ b/cumulusci/salesforce_api/soap_envelopes.py @@ -162,16 +162,17 @@ """ + METADATA_TYPES = """ - - - ###SESSION_ID### - - - - - {api_version} - - + + + ###SESSION_ID### + + + + + {api_version} + + """ diff --git a/cumulusci/salesforce_api/tests/metadata_test_strings.py b/cumulusci/salesforce_api/tests/metadata_test_strings.py index 4163fecdbb..977e76a295 100644 --- a/cumulusci/salesforce_api/tests/metadata_test_strings.py +++ b/cumulusci/salesforce_api/tests/metadata_test_strings.py @@ -20,5 +20,5 @@ deploy_status_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {process_id}\n true\n \n \n' -list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {api_version}\n \n \n' +list_metadata_types_envelope = '\n\n \n \n ###SESSION_ID###\n \n \n \n \n {api_version}\n \n \n' list_metadata_types_result = 'WorkflowFieldUpdateworkflowsfalsefalseworkflowWorkflowtruefalse' diff --git a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py index 99f6806a0d..326c71c9d6 100644 --- a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py +++ b/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py @@ -1,3 +1,5 @@ +import time + from cumulusci.salesforce_api.metadata import ApiListMetadataTypes from cumulusci.tasks.salesforce import BaseRetrieveMetadata @@ -24,6 +26,6 @@ def _run_task(self): api_object = self._get_api() while api_object.status == "Pending": - api_object() + time.sleep(1) self.logger.info("Metadata Types supported by org:\n" + str(api_object())) From d79eccd6d98ba05dd147874beb154558e377e99b Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 5 Oct 2023 17:46:54 +0530 Subject: [PATCH 47/98] Clean Up --- cumulusci/cumulusci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index fbe9493ebc..6ebb75e12d 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -482,7 +482,7 @@ tasks: class_path: cumulusci.tasks.salesforce.RetrieveMetadataTypes description: Retrieves the metadata types supported by the org based on the api version group: Salesforce Metadatatypes - + retrieve_src: description: Retrieves the packaged metadata into the src directory class_path: cumulusci.tasks.salesforce.RetrievePackaged @@ -490,7 +490,6 @@ tasks: path: src group: Salesforce Metadata - retrieve_unpackaged: description: Retrieve the contents of a package.xml file. class_path: cumulusci.tasks.salesforce.RetrieveUnpackaged @@ -689,6 +688,7 @@ tasks: mapping: "datasets/mapping.yml" sql_path: "datasets/sample.sql" group: "Data Operations" + load_dataset: description: Load a SQL dataset using the bulk API. class_path: cumulusci.tasks.bulkdata.load.LoadData From 8caa3abb8267a1927d598847e7de00f4385a4e00 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Mon, 9 Oct 2023 12:48:55 +0530 Subject: [PATCH 48/98] Changed Error Message --- cumulusci/cli/org.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 3d7514d348..1f8399c76e 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -573,7 +573,9 @@ def org_scratch( runtime.check_org_overwrite(org_name) release_options = ["previous", "preview"] if release and release not in release_options: - raise click.UsageError("Release options value is not valid.") + raise click.UsageError( + "Release options value is not valid. Either specify preview or previous." + ) scratch_configs = runtime.project_config.lookup("orgs__scratch") if not scratch_configs: From 12b8108ccdf6b713b683222d2ea0beba94e40cc5 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Mon, 9 Oct 2023 17:53:34 +0530 Subject: [PATCH 49/98] few_variable_changes --- .gitignore | 1 - cumulusci/cumulusci.yml | 10 +++------- cumulusci/salesforce_api/metadata.py | 5 ++--- cumulusci/salesforce_api/soap_envelopes.py | 3 +-- ...ieveMetadataTypes.py => DescribeMetadataTypes.py} | 12 ++++-------- cumulusci/tasks/salesforce/__init__.py | 2 +- ...etadatatypes.py => test_describemetadatatypes.py} | 8 ++++---- 7 files changed, 15 insertions(+), 26 deletions(-) rename cumulusci/tasks/salesforce/{RetrieveMetadataTypes.py => DescribeMetadataTypes.py} (77%) rename cumulusci/tasks/salesforce/tests/{test_retrievemetadatatypes.py => test_describemetadatatypes.py} (61%) diff --git a/.gitignore b/.gitignore index 2a85104501..d91ee2ee9b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ docs/api/ docs/_build/ github_release_notes.html tmp -cci_venv/ # Distribution / packaging *.egg diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 6ebb75e12d..6a3bf98422 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -477,19 +477,16 @@ tasks: options: path: packaged group: Salesforce Metadata - - retrieve_metadatatypes: - class_path: cumulusci.tasks.salesforce.RetrieveMetadataTypes + describe_metadatatypes: + class_path: cumulusci.tasks.salesforce.DescribeMetadataTypes description: Retrieves the metadata types supported by the org based on the api version - group: Salesforce Metadatatypes - + group: Salesforce Metadata retrieve_src: description: Retrieves the packaged metadata into the src directory class_path: cumulusci.tasks.salesforce.RetrievePackaged options: path: src group: Salesforce Metadata - retrieve_unpackaged: description: Retrieve the contents of a package.xml file. class_path: cumulusci.tasks.salesforce.RetrieveUnpackaged @@ -688,7 +685,6 @@ tasks: mapping: "datasets/mapping.yml" sql_path: "datasets/sample.sql" group: "Data Operations" - load_dataset: description: Load a SQL dataset using the bulk API. class_path: cumulusci.tasks.bulkdata.load.LoadData diff --git a/cumulusci/salesforce_api/metadata.py b/cumulusci/salesforce_api/metadata.py index ed82bd380b..7c40bbcd63 100644 --- a/cumulusci/salesforce_api/metadata.py +++ b/cumulusci/salesforce_api/metadata.py @@ -682,7 +682,7 @@ def _process_response(self, response): class ApiListMetadataTypes(BaseMetadataApiCall): check_interval = 1 - soap_envelope_start = soap_envelopes.METADATA_TYPES + soap_envelope_start = soap_envelopes.DESCRIBE_METADATA soap_action_start = "describemetadatatypes" def __init__(self, task, api_version=None): @@ -702,8 +702,7 @@ def _build_envelope_start(self): def _process_response(self, response): self.metadata_types = [] - response = response.content.decode("utf-8") - metaobjects = parseString(response).getElementsByTagName("metadataObjects") + metaobjects = parseString(response.content).getElementsByTagName("metadataObjects") for metadataobject in metaobjects: self.metadata_types.append( diff --git a/cumulusci/salesforce_api/soap_envelopes.py b/cumulusci/salesforce_api/soap_envelopes.py index 2f83e37be6..ae6e8ec816 100644 --- a/cumulusci/salesforce_api/soap_envelopes.py +++ b/cumulusci/salesforce_api/soap_envelopes.py @@ -162,8 +162,7 @@ """ - -METADATA_TYPES = """ +DESCRIBE_METADATA = """ diff --git a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py similarity index 77% rename from cumulusci/tasks/salesforce/RetrieveMetadataTypes.py rename to cumulusci/tasks/salesforce/DescribeMetadataTypes.py index 326c71c9d6..e92bd98b2c 100644 --- a/cumulusci/tasks/salesforce/RetrieveMetadataTypes.py +++ b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py @@ -1,10 +1,9 @@ -import time from cumulusci.salesforce_api.metadata import ApiListMetadataTypes from cumulusci.tasks.salesforce import BaseRetrieveMetadata -class RetrieveMetadataTypes(BaseRetrieveMetadata): +class DescribeMetadataTypes(BaseRetrieveMetadata): api_class = ApiListMetadataTypes task_options = { "api_version": { @@ -13,7 +12,7 @@ class RetrieveMetadataTypes(BaseRetrieveMetadata): } def _init_options(self, kwargs): - super(RetrieveMetadataTypes, self)._init_options(kwargs) + super(DescribeMetadataTypes, self)._init_options(kwargs) if "api_version" not in self.options: self.options[ "api_version" @@ -24,8 +23,5 @@ def _get_api(self): def _run_task(self): api_object = self._get_api() - - while api_object.status == "Pending": - time.sleep(1) - - self.logger.info("Metadata Types supported by org:\n" + str(api_object())) + metadata_list=api_object() + self.logger.info("Metadata Types supported by org:\n" + str(metadata_list)) diff --git a/cumulusci/tasks/salesforce/__init__.py b/cumulusci/tasks/salesforce/__init__.py index 41756fca2b..670c556600 100644 --- a/cumulusci/tasks/salesforce/__init__.py +++ b/cumulusci/tasks/salesforce/__init__.py @@ -30,7 +30,7 @@ "PublishCommunity": "cumulusci.tasks.salesforce.PublishCommunity", "RetrievePackaged": "cumulusci.tasks.salesforce.RetrievePackaged", "RetrieveReportsAndDashboards": "cumulusci.tasks.salesforce.RetrieveReportsAndDashboards", - "RetrieveMetadataTypes": "cumulusci.tasks.salesforce.RetrieveMetadataTypes", + "DescribeMetadataTypes": "cumulusci.tasks.salesforce.DescribeMetadataTypes", "RetrieveUnpackaged": "cumulusci.tasks.salesforce.RetrieveUnpackaged", "SOQLQuery": "cumulusci.tasks.salesforce.SOQLQuery", "SetTDTMHandlerStatus": "cumulusci.tasks.salesforce.trigger_handlers", diff --git a/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py b/cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py similarity index 61% rename from cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py rename to cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py index 07345aa2ca..0d3c86b25c 100644 --- a/cumulusci/tasks/salesforce/tests/test_retrievemetadatatypes.py +++ b/cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py @@ -1,18 +1,18 @@ from unittest import mock -from cumulusci.tasks.salesforce import RetrieveMetadataTypes +from cumulusci.tasks.salesforce import DescribeMetadataTypes from .util import create_task class TestRetrieveMetadataTypes: def test_run_task(self): - task = create_task(RetrieveMetadataTypes) + task = create_task(DescribeMetadataTypes) task._get_api = mock.Mock() task() task._get_api.assert_called_once() def test_run_task_with_apiversion(self): - task = create_task(RetrieveMetadataTypes, {"api_version": 8.0}) + task = create_task(DescribeMetadataTypes, {"api_version": 8.0}) assert task.options.get("api_version") == 8.0 - task._get_api() + task._get_api() \ No newline at end of file From d6a0a82a50c2e15eadb38e8840999aba1e27ecbd Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Mon, 9 Oct 2023 18:08:18 +0530 Subject: [PATCH 50/98] clean-up --- cumulusci/salesforce_api/metadata.py | 4 +++- cumulusci/tasks/salesforce/DescribeMetadataTypes.py | 3 +-- .../tasks/salesforce/tests/test_describemetadatatypes.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cumulusci/salesforce_api/metadata.py b/cumulusci/salesforce_api/metadata.py index 7c40bbcd63..8ef8c013bc 100644 --- a/cumulusci/salesforce_api/metadata.py +++ b/cumulusci/salesforce_api/metadata.py @@ -702,7 +702,9 @@ def _build_envelope_start(self): def _process_response(self, response): self.metadata_types = [] - metaobjects = parseString(response.content).getElementsByTagName("metadataObjects") + metaobjects = parseString(response.content).getElementsByTagName( + "metadataObjects" + ) for metadataobject in metaobjects: self.metadata_types.append( diff --git a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py index e92bd98b2c..49c605c37d 100644 --- a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py +++ b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py @@ -1,4 +1,3 @@ - from cumulusci.salesforce_api.metadata import ApiListMetadataTypes from cumulusci.tasks.salesforce import BaseRetrieveMetadata @@ -23,5 +22,5 @@ def _get_api(self): def _run_task(self): api_object = self._get_api() - metadata_list=api_object() + metadata_list = api_object() self.logger.info("Metadata Types supported by org:\n" + str(metadata_list)) diff --git a/cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py b/cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py index 0d3c86b25c..9715786b39 100644 --- a/cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py +++ b/cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py @@ -15,4 +15,4 @@ def test_run_task(self): def test_run_task_with_apiversion(self): task = create_task(DescribeMetadataTypes, {"api_version": 8.0}) assert task.options.get("api_version") == 8.0 - task._get_api() \ No newline at end of file + task._get_api() From b93d451711de84a510386124575a9d238629f8ad Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Thu, 12 Oct 2023 12:41:32 +0530 Subject: [PATCH 51/98] Fixed Log line issue for cci org info --- cumulusci/core/config/sfdx_org_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulusci/core/config/sfdx_org_config.py b/cumulusci/core/config/sfdx_org_config.py index ddf5c75871..3ff4f1634e 100644 --- a/cumulusci/core/config/sfdx_org_config.py +++ b/cumulusci/core/config/sfdx_org_config.py @@ -25,7 +25,7 @@ def sfdx_info(self): username = self.config.get("username") assert username is not None, "SfdxOrgConfig must have a username" - self.logger.info(f"Getting org info from Salesforce CLI for {username}") + # self.logger.info(f"Getting org info from Salesforce CLI for {username}") # Call force:org:display and parse output to get instance_url and # access_token From 2a9cadcb16d31a1f15f3a5dca0d50cf163bd1529 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Thu, 12 Oct 2023 13:15:58 +0530 Subject: [PATCH 52/98] added_clear_error --- cumulusci/tasks/create_package_version.py | 6 +++++- cumulusci/tasks/tests/conftest.py | 13 +++++++++++++ .../tasks/tests/test_create_package_version.py | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index ac958df95b..455642f2ba 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -144,6 +144,11 @@ class CreatePackageVersion(BaseSalesforceApiTask): def _init_options(self, kwargs): super()._init_options(kwargs) + if not self.org_config.config_file: + raise TaskOptionsError( + "Org doesn't contain a config_file. It's a persistent org like Devhub or Developer Edition org" + ) + # Allow these fields to be explicitly set to blanks # so that unlocked builds can override an otherwise-configured # postinstall script @@ -388,7 +393,6 @@ def _create_version_request( # Add org shape with open(self.org_config.config_file, "r") as f: scratch_org_def = json.load(f) - # See https://github.com/forcedotcom/packaging/blob/main/src/package/packageVersionCreate.ts#L358 # Note that we handle orgPreferences below by converting to settings, # in build_settings_package() diff --git a/cumulusci/tasks/tests/conftest.py b/cumulusci/tasks/tests/conftest.py index 0e73056036..9ae686af7f 100644 --- a/cumulusci/tasks/tests/conftest.py +++ b/cumulusci/tasks/tests/conftest.py @@ -28,3 +28,16 @@ def org_config(): ) org_config.refresh_oauth_token = mock.Mock() return org_config + + +@pytest.fixture +def persistent_org_config(): + persistent_org_config = OrgConfig( + { + "instance_url": "https://scratch.my.salesforce.com", + "access_token": "token", + }, + "alpha", + ) + persistent_org_config.refresh_oauth_token = mock.Mock() + return persistent_org_config diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index df901a35cb..1a74e546a6 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -126,6 +126,11 @@ def task(get_task): return get_task() +@pytest.fixture +def get_persistent_org_config(persistent_org_config): + return persistent_org_config + + @pytest.fixture def mock_download_extract_github(): with mock.patch( @@ -153,6 +158,18 @@ def mock_get_static_dependencies(): class TestPackageConfig: + def test_org_config(self, project_config, persistent_org_config): + with pytest.raises( + TaskOptionsError, + match="Org doesn't contain a config_file. It's a persistent org like Devhub or Developer Edition org", + ): + task = CreatePackageVersion( + project_config, + TaskConfig(), + persistent_org_config, + ) + task.__init__ + def test_validate_org_dependent(self): with pytest.raises(ValidationError, match="Only unlocked packages"): PackageConfig(package_type=PackageTypeEnum.managed, org_dependent=True) # type: ignore From 372791f7661e1d1b9d1e2c82f9caec368d260d5c Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Mon, 16 Oct 2023 16:46:45 +0530 Subject: [PATCH 53/98] Review Changes --- cumulusci/cli/org.py | 2 +- cumulusci/core/config/sfdx_org_config.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index 301e423581..e26df3d09e 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -305,7 +305,7 @@ def calculate_org_days(info): @pass_runtime(require_project=False, require_keychain=True) def org_info(runtime, org_name, print_json): org_name, org_config = runtime.get_org(org_name) - org_config.refresh_oauth_token(runtime.keychain) + org_config.refresh_oauth_token(runtime.keychain, print_json) console = Console() if print_json: click.echo( diff --git a/cumulusci/core/config/sfdx_org_config.py b/cumulusci/core/config/sfdx_org_config.py index 3ff4f1634e..323f5ab155 100644 --- a/cumulusci/core/config/sfdx_org_config.py +++ b/cumulusci/core/config/sfdx_org_config.py @@ -24,8 +24,8 @@ def sfdx_info(self): username = self.config.get("username") assert username is not None, "SfdxOrgConfig must have a username" - - # self.logger.info(f"Getting org info from Salesforce CLI for {username}") + if not self.print_json: + self.logger.info(f"Getting org info from Salesforce CLI for {username}") # Call force:org:display and parse output to get instance_url and # access_token @@ -196,7 +196,7 @@ def force_refresh_oauth_token(self): message = f"Message: {nl.join(stdout_list)}" raise SfdxOrgException(message) - def refresh_oauth_token(self, keychain): + def refresh_oauth_token(self, keychain, print_json): """Use sfdx force:org:describe to refresh token instead of built in OAuth handling""" if hasattr(self, "_sfdx_info"): # Cache the sfdx_info for 1 hour to avoid unnecessary calls out to sfdx CLI @@ -206,7 +206,7 @@ def refresh_oauth_token(self, keychain): # Force a token refresh self.force_refresh_oauth_token() - + self.print_json = print_json # Get org info via sfdx force:org:display self.sfdx_info # Get additional org info by querying API From b6c4d57d8836a9897c42f72916c7e647700d3d60 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 17 Oct 2023 10:36:21 +0530 Subject: [PATCH 54/98] Test Fixes --- cumulusci/core/config/sfdx_org_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulusci/core/config/sfdx_org_config.py b/cumulusci/core/config/sfdx_org_config.py index 323f5ab155..5a82f4134a 100644 --- a/cumulusci/core/config/sfdx_org_config.py +++ b/cumulusci/core/config/sfdx_org_config.py @@ -196,7 +196,7 @@ def force_refresh_oauth_token(self): message = f"Message: {nl.join(stdout_list)}" raise SfdxOrgException(message) - def refresh_oauth_token(self, keychain, print_json): + def refresh_oauth_token(self, keychain, print_json=False): """Use sfdx force:org:describe to refresh token instead of built in OAuth handling""" if hasattr(self, "_sfdx_info"): # Cache the sfdx_info for 1 hour to avoid unnecessary calls out to sfdx CLI From dc8c550cbf09669a479644716395f905a2ca9cd3 Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Wed, 18 Oct 2023 15:52:21 +0530 Subject: [PATCH 55/98] Tf --- cumulusci/core/config/sfdx_org_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cumulusci/core/config/sfdx_org_config.py b/cumulusci/core/config/sfdx_org_config.py index 5a82f4134a..c466778047 100644 --- a/cumulusci/core/config/sfdx_org_config.py +++ b/cumulusci/core/config/sfdx_org_config.py @@ -196,6 +196,7 @@ def force_refresh_oauth_token(self): message = f"Message: {nl.join(stdout_list)}" raise SfdxOrgException(message) + # Added a print json argument to check whether it is there or not def refresh_oauth_token(self, keychain, print_json=False): """Use sfdx force:org:describe to refresh token instead of built in OAuth handling""" if hasattr(self, "_sfdx_info"): From 1459a3292a63728e2122c3718b5dd94a1f9f409d Mon Sep 17 00:00:00 2001 From: James Estevez Date: Mon, 23 Oct 2023 12:46:56 -0700 Subject: [PATCH 56/98] W42 dependency updates (#3686) * W42 dependency updates * Sweep transitive dependency conflict under the rug TODO: We should split extras into their own requirements files since we're only doing this to make sure the dependency's version is the same in prod as under test. * Update for typeguard v4 * We're gonna need a bigger rug * Updates for Robot Framework 6.1 1. Robot framework team removed the add_path function in robotframework/robotframework@9a87af, this commit adds a utility function with the same behavior. 2. Probably unrelated to 6.1, but keyword.source is a Path instance now, so we needed to convert it to a str for a comparison. 3. Latest flake8 includes a type comparison rule. * Stop typechecking libraries problematic in pyright Updating to pyright<=1.1.306 made it so much smarter that it broke several files: - sarge.Command: uses setattr to dynamically add stderr_text - sqlalchemy: see [this issue]. - github3: Return type for GitHub.repository() is actually `Repository | None` Removing instead of using `pyright: ignore` because the last two are actually valid errors. [this issue]: https://github.com/microsoft/pyright/issues 5062#issuecomment-1623779925 * Remove SFStandardContact macro I have no idea what's happening here. At first glance it appears to be a regression, but I can't reproduce within Snowfakery's repo on 3.5.0 or 3.4.0. --- .../snowfakery/query_snowfakery.recipe.yml | 1 - .../tasks/marketing_cloud/get_user_info.py | 2 +- cumulusci/tasks/robotframework/libdoc.py | 2 +- .../tasks/robotframework/robotframework.py | 21 ++- .../tests/test_robotframework.py | 10 +- pyproject.toml | 17 +- requirements/dev.txt | 178 +++++++++--------- requirements/prod.txt | 88 ++++----- 8 files changed, 160 insertions(+), 159 deletions(-) diff --git a/cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml b/cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml index 5cee11d9cc..605022a5bb 100644 --- a/cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml +++ b/cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml @@ -3,7 +3,6 @@ # contact based on a user - object: Contact - include: SFStandardContact fields: __user: SalesforceQuery.find_record: diff --git a/cumulusci/tasks/marketing_cloud/get_user_info.py b/cumulusci/tasks/marketing_cloud/get_user_info.py index efe49f40c1..e44b7d1082 100644 --- a/cumulusci/tasks/marketing_cloud/get_user_info.py +++ b/cumulusci/tasks/marketing_cloud/get_user_info.py @@ -14,7 +14,7 @@ def _run_task(self): payload = self.mc_config.get_user_info() except requests.exceptions.HTTPError as e: self.logger.error( - f"Exception occurred fetching user info: {e.response.text}" + f"Exception occurred fetching user info: {e.response.text if e.response else 'None'}" ) raise diff --git a/cumulusci/tasks/robotframework/libdoc.py b/cumulusci/tasks/robotframework/libdoc.py index 91c4f5eb0f..876a09cf39 100644 --- a/cumulusci/tasks/robotframework/libdoc.py +++ b/cumulusci/tasks/robotframework/libdoc.py @@ -241,7 +241,7 @@ def to_tuples(self): # we don't want to see the same base pageobject # keywords a kajillion times. This should probably # be configurable, but I don't need it to be right now. - if base_pageobjects_path in keyword.source: + if base_pageobjects_path in str(keyword.source): continue path = Path(keyword.source) diff --git a/cumulusci/tasks/robotframework/robotframework.py b/cumulusci/tasks/robotframework/robotframework.py index 3cc76e9768..f05d5d11d5 100644 --- a/cumulusci/tasks/robotframework/robotframework.py +++ b/cumulusci/tasks/robotframework/robotframework.py @@ -1,3 +1,4 @@ +import fnmatch import json import os import shlex @@ -5,7 +6,6 @@ import sys from pathlib import Path -from robot import pythonpathsetter from robot import run as robot_run from robot.testdoc import testdoc @@ -278,11 +278,8 @@ def _run_task(self): # Save it so that we can restore it later orig_sys_path = sys.path.copy() - # Add each source to PYTHONPATH. Robot recommends that we - # use pythonpathsetter instead of directly setting - # sys.path. for path in source_paths.values(): - pythonpathsetter.add_path(path, end=True) + add_path(path, end=True) # Make sure the path to the repo root is on sys.path. Normally # it will be, but if we're running this task from another repo @@ -293,7 +290,7 @@ def _run_task(self): # by robot.run. Plus, robot recommends we call a special # function instead of directly modifying sys.path if self.project_config.repo_root not in sys.path: - pythonpathsetter.add_path(self.project_config.repo_root) + add_path(self.project_config.repo_root) options["stdout"] = sys.stdout options["stderr"] = sys.stderr @@ -380,4 +377,16 @@ def execute_script(self, script, *args): WebDriver.execute_script = execute_script +def add_path(path, end=False): + """ + Adds a source to PYTHONPATH. Robot recommended that we + use pythonpathsetter.add_path instead of directly setting + sys.path, but removed this function in v6.1. + """ + if not end: + sys.path.insert(0, path) + elif not any(fnmatch.fnmatch(p, path) for p in sys.path): + sys.path.append(path) + + patch_executescript() diff --git a/cumulusci/tasks/robotframework/tests/test_robotframework.py b/cumulusci/tasks/robotframework/tests/test_robotframework.py index 2068ccc108..0a9a4e7a74 100644 --- a/cumulusci/tasks/robotframework/tests/test_robotframework.py +++ b/cumulusci/tasks/robotframework/tests/test_robotframework.py @@ -182,7 +182,7 @@ def test_tagstatexclude(self, mock_robot_run): }, }, ) - assert type(task.options["options"]["tagstatexclude"]) == list + assert type(task.options["options"]["tagstatexclude"]) is list task() outputdir = str(Path(".").resolve()) mock_robot_run.assert_called_once_with( @@ -266,9 +266,7 @@ def test_user_defined_listeners_option(self): assert KeywordLogger in listener_classes @mock.patch("cumulusci.tasks.robotframework.robotframework.robot_run") - @mock.patch( - "cumulusci.tasks.robotframework.robotframework.pythonpathsetter.add_path" - ) + @mock.patch("cumulusci.tasks.robotframework.robotframework.add_path") def test_sources(self, mock_add_path, mock_robot_run): """Verify that sources get added to PYTHONPATH when task runs""" universal_config = UniversalConfig() @@ -313,9 +311,7 @@ def test_sources(self, mock_add_path, mock_robot_run): ) @mock.patch("cumulusci.tasks.robotframework.robotframework.robot_run") - @mock.patch( - "cumulusci.tasks.robotframework.robotframework.pythonpathsetter.add_path" - ) + @mock.patch("cumulusci.tasks.robotframework.robotframework.add_path") def test_repo_root_in_sys_path(self, mock_add_path, mock_robot_run): """Verify that the repo root is added to sys.path diff --git a/pyproject.toml b/pyproject.toml index e0b65ce47f..3b494008e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,10 @@ dependencies = [ "keyring<=23.0.1", "defusedxml", "lxml", + "markdown-it-py==2.2.0", # resolve dependency conflict between prod/dev "MarkupSafe", "psutil", - "pydantic", + "pydantic<2", "PyJWT", "pytz", "pyyaml", @@ -73,7 +74,7 @@ test = [ "responses", "testfixtures", "tox", - "typeguard", + "typeguard<=2.13.3", # TODO: Lots of changes required for v4 "vcrpy" ] @@ -173,8 +174,6 @@ include = [ 'cumulusci/core/config/base_config.py', 'cumulusci/core/config/base_task_flow_config.py', 'cumulusci/core/config/oauth2_service_config.py', - 'cumulusci/core/config/scratch_org_config.py', - 'cumulusci/core/config/sfdx_org_config.py', 'cumulusci/core/debug.py', 'cumulusci/core/dependencies/__init__.py', 'cumulusci/core/dependencies/utils.py', @@ -184,7 +183,6 @@ include = [ 'cumulusci/core/metadeploy/__init__.py', 'cumulusci/core/metadeploy/api.py', 'cumulusci/core/runtime.py', - 'cumulusci/core/sfdx.py', 'cumulusci/core/source/__init__.py', 'cumulusci/core/source/local_folder.py', 'cumulusci/core/source_transforms/__init__.py', @@ -199,7 +197,6 @@ include = [ 'cumulusci/salesforce_api/exceptions.py', 'cumulusci/salesforce_api/filterable_objects.py', 'cumulusci/salesforce_api/mc_soap_envelopes.py', - 'cumulusci/salesforce_api/org_schema_models.py', 'cumulusci/salesforce_api/package_install.py', 'cumulusci/salesforce_api/soap_envelopes.py', 'cumulusci/tasks/__init__.py', @@ -212,17 +209,10 @@ include = [ 'cumulusci/tasks/bulkdata/generate_mapping_utils/extract_mapping_file_generator.py', 'cumulusci/tasks/bulkdata/generate_mapping_utils/generate_mapping_from_declarations.py', 'cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_generator_post_processes.py', - 'cumulusci/tasks/bulkdata/query_transformers.py', - 'cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_working_directory.py', 'cumulusci/tasks/bulkdata/snowfakery_utils/subtask_configurator.py', 'cumulusci/tasks/dx_convert_from.py', 'cumulusci/tasks/github/__init__.py', 'cumulusci/tasks/github/commit_status.py', - 'cumulusci/tasks/github/merge.py', - 'cumulusci/tasks/github/publish.py', - 'cumulusci/tasks/github/pull_request.py', - 'cumulusci/tasks/github/release_report.py', - 'cumulusci/tasks/github/tag.py', 'cumulusci/tasks/github/util.py', 'cumulusci/tasks/marketing_cloud/__init__.py', 'cumulusci/tasks/marketing_cloud/api.py', @@ -263,7 +253,6 @@ include = [ 'cumulusci/tests/util.py', 'cumulusci/utils/waiting.py', 'cumulusci/utils/xml/robot_xml.py', - 'cumulusci/utils/xml/salesforce_encoding.py', 'cumulusci/utils/ziputils.py' ] # Do not add to this list. Instead use diff --git a/requirements/dev.txt b/requirements/dev.txt index 95efd2dd81..626507d8e5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,74 +2,80 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --all-extras --output-file=requirements/dev.txt --resolver=backtracking pyproject.toml +# pip-compile --all-extras --output-file=requirements/dev.txt pyproject.toml # alabaster==0.7.13 # via sphinx appdirs==1.4.4 # via fs -attrs==22.2.0 +attrs==23.1.0 # via # jsonschema # pytest -authlib==1.2.0 + # referencing +authlib==1.2.1 # via simple-salesforce -babel==2.11.0 +babel==2.13.0 # via sphinx -beautifulsoup4==4.11.2 +beautifulsoup4==4.12.2 # via furo -black==23.1.0 +black==23.10.0 # via cumulusci (pyproject.toml) -cachetools==5.3.0 +cachetools==5.3.1 # via tox -certifi==2022.12.7 +certifi==2023.7.22 # via # requests # snowfakery -cffi==1.15.1 +cffi==1.16.0 # via cryptography -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -chardet==5.1.0 +chardet==5.2.0 # via tox -charset-normalizer==3.0.1 +charset-normalizer==3.2.0 # via # requests # snowfakery -click==8.1.3 +click==8.1.6 # via # black # cumulusci (pyproject.toml) # snowfakery colorama==0.4.6 # via tox -coverage[toml]==7.1.0 +coverage[toml]==7.3.2 # via # cumulusci (pyproject.toml) # pytest-cov -cryptography==39.0.1 +cryptography==41.0.4 # via # authlib # cumulusci (pyproject.toml) # pyjwt - # secretstorage defusedxml==0.7.1 # via cumulusci (pyproject.toml) -distlib==0.3.6 +distlib==0.3.7 # via virtualenv docutils==0.16 # via # cumulusci (pyproject.toml) # myst-parser # sphinx -factory-boy==3.2.1 +factory-boy==3.3.0 # via cumulusci (pyproject.toml) -faker==17.0.0 +faker==19.3.0 # via # cumulusci (pyproject.toml) # factory-boy + # faker-edu + # faker-nonprofit # snowfakery -filelock==3.9.0 +faker-edu==1.0.0 + # via snowfakery +faker-nonprofit==1.0.0 + # via snowfakery +filelock==3.12.4 # via # tox # virtualenv @@ -77,17 +83,15 @@ flake8==3.9.2 # via cumulusci (pyproject.toml) fs==2.4.16 # via cumulusci (pyproject.toml) -furo==2022.12.7 +furo==2023.3.27 # via cumulusci (pyproject.toml) -github3-py==3.2.0 +github3-py==4.0.1 # via cumulusci (pyproject.toml) -greenlet==2.0.2 - # via - # snowfakery - # sqlalchemy +greenlet==3.0.0 + # via sqlalchemy gvgen==1.0 # via snowfakery -identify==2.5.18 +identify==2.5.30 # via pre-commit idna==3.4 # via @@ -96,45 +100,46 @@ idna==3.4 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==6.0.0 +importlib-metadata==6.8.0 # via # keyring # sphinx -importlib-resources==5.10.2 - # via jsonschema +importlib-resources==6.1.0 + # via + # jsonschema + # jsonschema-specifications iniconfig==2.0.0 # via pytest isort==5.12.0 # via cumulusci (pyproject.toml) -jeepney==0.8.0 - # via - # keyring - # secretstorage jinja2==3.1.2 # via # cumulusci (pyproject.toml) # myst-parser # snowfakery # sphinx -jsonschema==4.17.3 +jsonschema==4.19.1 # via cumulusci (pyproject.toml) +jsonschema-specifications==2023.7.1 + # via jsonschema keyring==23.0.1 # via cumulusci (pyproject.toml) -lxml==4.9.2 +lxml==4.9.3 # via cumulusci (pyproject.toml) -markdown-it-py==2.1.0 +markdown-it-py==2.2.0 # via + # cumulusci (pyproject.toml) # mdit-py-plugins # myst-parser # rich -markupsafe==2.1.2 +markupsafe==2.1.3 # via # cumulusci (pyproject.toml) # jinja2 # snowfakery mccabe==0.6.1 # via flake8 -mdit-py-plugins==0.3.3 +mdit-py-plugins==0.3.5 # via myst-parser mdurl==0.1.2 # via markdown-it-py @@ -142,35 +147,35 @@ multidict==6.0.4 # via yarl mypy-extensions==1.0.0 # via black -myst-parser==0.18.1 +myst-parser==1.0.0 # via cumulusci (pyproject.toml) -natsort==8.2.0 +natsort==8.4.0 # via robotframework-pabot -nodeenv==1.7.0 +nodeenv==1.8.0 # via pre-commit -packaging==23.0 +packaging==23.2 # via # black # pyproject-api # pytest # sphinx # tox -pathspec==0.11.0 +pathspec==0.11.2 # via black pkgutil-resolve-name==1.3.10 # via jsonschema -platformdirs==3.0.0 +platformdirs==3.11.0 # via # black # tox # virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via # pytest # tox -pre-commit==3.0.4 +pre-commit==3.5.0 # via cumulusci (pyproject.toml) -psutil==5.9.4 +psutil==5.9.6 # via cumulusci (pyproject.toml) py==1.11.0 # via pytest @@ -178,32 +183,30 @@ pycodestyle==2.7.0 # via flake8 pycparser==2.21 # via cffi -pydantic==1.10.5 +pydantic==1.10.12 # via # cumulusci (pyproject.toml) # snowfakery pyflakes==2.3.1 # via flake8 -pygments==2.14.0 +pygments==2.16.1 # via # furo # rich # sphinx -pyjwt[crypto]==2.6.0 +pyjwt[crypto]==2.8.0 # via # cumulusci (pyproject.toml) # github3-py -pyproject-api==1.5.0 +pyproject-api==1.6.1 # via tox -pyrsistent==0.19.3 - # via jsonschema pytest==7.0.1 # via # cumulusci (pyproject.toml) # pytest-cov # pytest-random-order # pytest-vcr -pytest-cov==4.0.0 +pytest-cov==4.1.0 # via cumulusci (pyproject.toml) pytest-random-order==1.1.0 # via cumulusci (pyproject.toml) @@ -217,18 +220,23 @@ python-dateutil==2.8.2 # faker # github3-py # snowfakery -pytz==2022.7.1 +pytz==2023.3.post1 # via # babel # cumulusci (pyproject.toml) -pyyaml==6.0 +pyyaml==6.0.1 # via # cumulusci (pyproject.toml) # myst-parser # pre-commit + # responses # snowfakery # vcrpy -requests==2.28.2 +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications +requests==2.29.0 # via # cumulusci (pyproject.toml) # github3-py @@ -239,13 +247,13 @@ requests==2.28.2 # simple-salesforce # snowfakery # sphinx -requests-futures==1.0.0 +requests-futures==1.0.1 # via cumulusci (pyproject.toml) -responses==0.22.0 +responses==0.23.1 # via cumulusci (pyproject.toml) -rich==13.3.1 +rich==13.6.0 # via cumulusci (pyproject.toml) -robotframework==6.0.2 +robotframework==6.1.1 # via # cumulusci (pyproject.toml) # robotframework-lint @@ -255,24 +263,26 @@ robotframework==6.0.2 # robotframework-stacktrace robotframework-lint==1.1 # via cumulusci (pyproject.toml) -robotframework-pabot==2.13.0 +robotframework-pabot==2.16.0 # via cumulusci (pyproject.toml) -robotframework-pythonlibcore==4.1.0 +robotframework-pythonlibcore==4.2.0 # via robotframework-seleniumlibrary -robotframework-requests==0.9.4 +robotframework-requests==0.9.5 # via cumulusci (pyproject.toml) robotframework-seleniumlibrary==5.1.3 # via cumulusci (pyproject.toml) robotframework-stacktrace==0.4.1 # via robotframework-pabot +rpds-py==0.10.6 + # via + # jsonschema + # referencing rst2ansi==0.1.5 # via cumulusci (pyproject.toml) salesforce-bulk==2.2.0 # via cumulusci (pyproject.toml) sarge==0.1.7.post1 # via cumulusci (pyproject.toml) -secretstorage==3.3.3 - # via keyring selenium==3.141.0 # via # cumulusci (pyproject.toml) @@ -287,12 +297,11 @@ six==1.16.0 # python-dateutil # salesforce-bulk # snowfakery - # vcrpy snowballstemmer==2.2.0 # via sphinx -snowfakery==3.5.0 +snowfakery==3.6.1 # via cumulusci (pyproject.toml) -soupsieve==2.4 +soupsieve==2.5 # via beautifulsoup4 sphinx==5.3.0 # via @@ -300,7 +309,7 @@ sphinx==5.3.0 # furo # myst-parser # sphinx-basic-ng -sphinx-basic-ng==1.0.0b1 +sphinx-basic-ng==1.0.0b2 # via furo sphinxcontrib-applehelp==1.0.4 # via sphinx @@ -314,14 +323,12 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sqlalchemy==1.4.46 +sqlalchemy==1.4.49 # via # cumulusci (pyproject.toml) # snowfakery -testfixtures==7.1.0 +testfixtures==7.2.0 # via cumulusci (pyproject.toml) -toml==0.10.2 - # via responses tomli==2.0.1 # via # black @@ -329,16 +336,16 @@ tomli==2.0.1 # pyproject-api # pytest # tox -tox==4.4.5 +tox==4.11.3 # via cumulusci (pyproject.toml) typeguard==2.13.3 # via cumulusci (pyproject.toml) -types-toml==0.10.8.4 +types-pyyaml==6.0.12.12 # via responses -typing-extensions==4.5.0 +typing-extensions==4.7.1 # via # black - # myst-parser + # faker # pydantic # rich # snowfakery @@ -346,27 +353,28 @@ unicodecsv==0.14.1 # via salesforce-bulk uritemplate==4.1.1 # via github3-py -urllib3==1.26.14 +urllib3==1.26.16 # via # requests # responses # selenium # snowfakery -vcrpy==4.2.1 + # vcrpy +vcrpy==5.1.0 # via # cumulusci (pyproject.toml) # pytest-vcr -virtualenv==20.19.0 +virtualenv==20.24.5 # via # pre-commit # tox -wrapt==1.14.1 +wrapt==1.15.0 # via vcrpy xmltodict==0.13.0 # via cumulusci (pyproject.toml) -yarl==1.8.2 +yarl==1.9.2 # via vcrpy -zipp==3.13.0 +zipp==3.17.0 # via # importlib-metadata # importlib-resources diff --git a/requirements/prod.txt b/requirements/prod.txt index 2da9f813d5..ebb157e3d8 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -2,90 +2,91 @@ # This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --output-file=requirements/prod.txt --resolver=backtracking pyproject.toml +# pip-compile --output-file=requirements/prod.txt pyproject.toml # appdirs==1.4.4 # via fs -authlib==1.2.0 +authlib==1.2.1 # via simple-salesforce -certifi==2022.12.7 +certifi==2023.7.22 # via # requests # snowfakery -cffi==1.15.1 +cffi==1.16.0 # via cryptography -charset-normalizer==3.0.1 +charset-normalizer==3.2.0 # via # requests # snowfakery -click==8.1.3 +click==8.1.6 # via # cumulusci (pyproject.toml) # snowfakery -cryptography==39.0.1 +cryptography==41.0.4 # via # authlib # cumulusci (pyproject.toml) # pyjwt - # secretstorage defusedxml==0.7.1 # via cumulusci (pyproject.toml) docutils==0.16 # via cumulusci (pyproject.toml) -faker==17.0.0 +faker==19.3.0 # via # cumulusci (pyproject.toml) + # faker-edu + # faker-nonprofit # snowfakery +faker-edu==1.0.0 + # via snowfakery +faker-nonprofit==1.0.0 + # via snowfakery fs==2.4.16 # via cumulusci (pyproject.toml) -github3-py==3.2.0 +github3-py==4.0.1 # via cumulusci (pyproject.toml) -greenlet==2.0.2 - # via - # snowfakery - # sqlalchemy +greenlet==3.0.0 + # via sqlalchemy gvgen==1.0 # via snowfakery idna==3.4 # via # requests # snowfakery -importlib-metadata==6.0.0 +importlib-metadata==6.8.0 # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage jinja2==3.1.2 # via # cumulusci (pyproject.toml) # snowfakery keyring==23.0.1 # via cumulusci (pyproject.toml) -lxml==4.9.2 +lxml==4.9.3 # via cumulusci (pyproject.toml) -markdown-it-py==2.1.0 - # via rich -markupsafe==2.1.2 +markdown-it-py==2.2.0 + # via + # cumulusci (pyproject.toml) + # rich +markupsafe==2.1.3 # via # cumulusci (pyproject.toml) # jinja2 # snowfakery mdurl==0.1.2 # via markdown-it-py -natsort==8.2.0 +natsort==8.4.0 # via robotframework-pabot -psutil==5.9.4 +psutil==5.9.6 # via cumulusci (pyproject.toml) pycparser==2.21 # via cffi -pydantic==1.10.5 +pydantic==1.10.12 # via # cumulusci (pyproject.toml) # snowfakery -pygments==2.14.0 +pygments==2.16.1 # via rich -pyjwt[crypto]==2.6.0 +pyjwt[crypto]==2.8.0 # via # cumulusci (pyproject.toml) # github3-py @@ -97,13 +98,13 @@ python-dateutil==2.8.2 # faker # github3-py # snowfakery -pytz==2022.7.1 +pytz==2023.3.post1 # via cumulusci (pyproject.toml) -pyyaml==6.0 +pyyaml==6.0.1 # via # cumulusci (pyproject.toml) # snowfakery -requests==2.28.2 +requests==2.29.0 # via # cumulusci (pyproject.toml) # github3-py @@ -112,11 +113,11 @@ requests==2.28.2 # salesforce-bulk # simple-salesforce # snowfakery -requests-futures==1.0.0 +requests-futures==1.0.1 # via cumulusci (pyproject.toml) -rich==13.3.1 +rich==13.6.0 # via cumulusci (pyproject.toml) -robotframework==6.0.2 +robotframework==6.1.1 # via # cumulusci (pyproject.toml) # robotframework-lint @@ -126,11 +127,11 @@ robotframework==6.0.2 # robotframework-stacktrace robotframework-lint==1.1 # via cumulusci (pyproject.toml) -robotframework-pabot==2.13.0 +robotframework-pabot==2.16.0 # via cumulusci (pyproject.toml) -robotframework-pythonlibcore==4.1.0 +robotframework-pythonlibcore==4.2.0 # via robotframework-seleniumlibrary -robotframework-requests==0.9.4 +robotframework-requests==0.9.5 # via cumulusci (pyproject.toml) robotframework-seleniumlibrary==5.1.3 # via cumulusci (pyproject.toml) @@ -142,8 +143,6 @@ salesforce-bulk==2.2.0 # via cumulusci (pyproject.toml) sarge==0.1.7.post1 # via cumulusci (pyproject.toml) -secretstorage==3.3.3 - # via keyring selenium==3.141.0 # via # cumulusci (pyproject.toml) @@ -158,14 +157,15 @@ six==1.16.0 # python-dateutil # salesforce-bulk # snowfakery -snowfakery==3.5.0 +snowfakery==3.6.1 # via cumulusci (pyproject.toml) -sqlalchemy==1.4.46 +sqlalchemy==1.4.49 # via # cumulusci (pyproject.toml) # snowfakery -typing-extensions==4.5.0 +typing-extensions==4.7.1 # via + # faker # pydantic # rich # snowfakery @@ -173,14 +173,14 @@ unicodecsv==0.14.1 # via salesforce-bulk uritemplate==4.1.1 # via github3-py -urllib3==1.26.14 +urllib3==1.26.16 # via # requests # selenium # snowfakery xmltodict==0.13.0 # via cumulusci (pyproject.toml) -zipp==3.13.0 +zipp==3.17.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: From 520b51128c7702a374da9a8838f38890082fd3de Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Wed, 25 Oct 2023 00:54:16 +0530 Subject: [PATCH 57/98] Fix `TypeError` when service sensitive attribute is `None` (#3674) Fixed the exception for the command cci service info connected_app while built_in is default Co-authored-by: James Estevez --- cumulusci/cli/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulusci/cli/service.py b/cumulusci/cli/service.py index e85280f2e3..39bd2040a0 100644 --- a/cumulusci/cli/service.py +++ b/cumulusci/cli/service.py @@ -371,7 +371,7 @@ def get_service_data(service_config, sensitive_attributes) -> list: click.style(k, bold=True), ( (v[:5] + (len(v[5:]) * "*") if len(v) > 10 else "*" * len(v)) - if k in sensitive_attributes + if k in sensitive_attributes and v is not None else str(v) ), ] From ee44ec14650f8217428e9a1d378c37d762655dbc Mon Sep 17 00:00:00 2001 From: lakshmi2506 <141401869+lakshmi2506@users.noreply.github.com> Date: Wed, 25 Oct 2023 05:05:57 +0530 Subject: [PATCH 58/98] metadeploy_publish presents clear errors when plans are not available (#3684) --- cumulusci/tasks/metadeploy.py | 11 +++++-- cumulusci/tasks/tests/test_metadeploy.py | 42 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cumulusci/tasks/metadeploy.py b/cumulusci/tasks/metadeploy.py index e55154422f..53f87c3877 100644 --- a/cumulusci/tasks/metadeploy.py +++ b/cumulusci/tasks/metadeploy.py @@ -83,12 +83,21 @@ def _init_task(self): Path(self.labels_path).mkdir(parents=True, exist_ok=True) if plan_name := self.options.get("plan"): + if self.project_config.lookup(f"plans__{plan_name}") is None: + raise TaskOptionsError( + f"Plan {plan_name} not found in project configuration" + ) plan_configs = { plan_name: self.project_config.lookup(f"plans__{plan_name}") } self.plan_configs = plan_configs else: self.plan_configs = self.project_config.plans + # Handled exception for no plan + if self.plan_configs is None or len(self.plan_configs) == 0: + raise CumulusCIException( + "No plan found to publish in project configuration" + ) self._load_labels() @@ -107,7 +116,6 @@ def _run_task(self): raise CumulusCIException( f"No slug found in MetaDeploy for product {product} from {repo_url}" ) - if not self.dry_run: version = self._find_or_create_version(product) if self.labels_path and "slug" in product: @@ -138,7 +146,6 @@ def _run_task(self): ) project_config.set_keychain(self.project_config.keychain) - # Create each plan for plan_name, plan_config in self.plan_configs.items(): self._add_plan_labels( plan_name=plan_name, diff --git a/cumulusci/tasks/tests/test_metadeploy.py b/cumulusci/tasks/tests/test_metadeploy.py index 785dba131e..bd39b62a35 100644 --- a/cumulusci/tasks/tests/test_metadeploy.py +++ b/cumulusci/tasks/tests/test_metadeploy.py @@ -439,6 +439,14 @@ def test_find_product__not_found(self): ) project_config = create_project_config() project_config.config["project"]["git"]["repo_url"] = "EXISTING_REPO" + project_config.config["plans"] = { + "install": { + "title": "Test Install", + "slug": "install", + "tier": "primary", + "steps": {1: {"flow": "install_prod"}}, + } + } project_config.keychain.set_service( "metadeploy", "test_alias", @@ -485,6 +493,37 @@ def test_init_task__named_plan(self): task._init_task() assert expected_plans == task.plan_configs + @pytest.mark.parametrize( + "options, errortype,errormsg", + [ + ( + {"tag": "release/1.0"}, + CumulusCIException, + "No plan found to publish in project configuration", + ), + ( + {"tag": "release/1.0", "plan": "install"}, + TaskOptionsError, + "Plan install not found in project configuration", + ), + ], + ) + def test_init_task_no_plan(self, options, errortype, errormsg): + project_config = create_project_config() + project_config.config["project"]["git"]["repo_url"] = "EXISTING_REPO" + project_config.keychain.set_service( + "metadeploy", + "test_alias", + ServiceConfig({"url": "https://metadeploy", "token": "TOKEN"}), + ) + task_config = TaskConfig({"options": options}) + task = Publish(project_config, task_config) + with pytest.raises( + errortype, + match=errormsg, + ): + task._init_task() + @responses.activate def test_find_or_create_plan_template__not_found(self): responses.add( @@ -540,6 +579,9 @@ def test_freeze_steps__skip(self): "tier": "primary", "steps": {1: {"task": "None"}}, } + project_config.config["plans"] = { + "Test Install": plan_config, + } task_config = TaskConfig({"options": {"tag": "release/1.0"}}) task = Publish(project_config, task_config) task._init_task() From 8f62d315383b331607a514779813a789f1106aed Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Wed, 25 Oct 2023 12:29:13 +0530 Subject: [PATCH 59/98] changed error message & added task_docs --- cumulusci/tasks/create_package_version.py | 15 ++++++++++++--- cumulusci/tasks/tests/conftest.py | 13 ------------- .../tasks/tests/test_create_package_version.py | 15 ++++++--------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index 455642f2ba..019cb8a9fe 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -41,6 +41,9 @@ format_subscriber_package_version_where_clause, ) +PERSISTANT_ORG_ERROR=""" +Target org scratch org definition file missing. Persistent orgs e.g.,Dev Hub can't be used for 2GP package uploads. +""" class PackageTypeEnum(StrEnum): managed = "Managed" @@ -83,6 +86,14 @@ class CreatePackageVersion(BaseSalesforceApiTask): If a package named ``package_name`` does not yet exist in the Dev Hub, it will be created. """ + task_docs = """ + Facilitates the upload of 2GP (second-generation packaging) + package versions using CumulusCI. + + The target org is used both for looking up dependency package IDs and + configuring the build org during the package upload. Ensure the specified + org is a scratch org with the correct configuration for these purposes. + """ api_version = "52.0" @@ -145,9 +156,7 @@ def _init_options(self, kwargs): super()._init_options(kwargs) if not self.org_config.config_file: - raise TaskOptionsError( - "Org doesn't contain a config_file. It's a persistent org like Devhub or Developer Edition org" - ) + raise TaskOptionsError(PERSISTANT_ORG_ERROR) # Allow these fields to be explicitly set to blanks # so that unlocked builds can override an otherwise-configured diff --git a/cumulusci/tasks/tests/conftest.py b/cumulusci/tasks/tests/conftest.py index 9ae686af7f..0e73056036 100644 --- a/cumulusci/tasks/tests/conftest.py +++ b/cumulusci/tasks/tests/conftest.py @@ -28,16 +28,3 @@ def org_config(): ) org_config.refresh_oauth_token = mock.Mock() return org_config - - -@pytest.fixture -def persistent_org_config(): - persistent_org_config = OrgConfig( - { - "instance_url": "https://scratch.my.salesforce.com", - "access_token": "token", - }, - "alpha", - ) - persistent_org_config.refresh_oauth_token = mock.Mock() - return persistent_org_config diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index 1a74e546a6..39a3b71d26 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -12,7 +12,7 @@ import yaml from pydantic import ValidationError -from cumulusci.core.config import BaseProjectConfig, TaskConfig, UniversalConfig +from cumulusci.core.config import BaseProjectConfig, OrgConfig, TaskConfig, UniversalConfig from cumulusci.core.dependencies.dependencies import ( PackageNamespaceVersionDependency, PackageVersionIdDependency, @@ -32,6 +32,7 @@ PackageConfig, PackageTypeEnum, VersionTypeEnum, + PERSISTANT_ORG_ERROR, ) from cumulusci.utils import temporary_dir, touch @@ -126,11 +127,6 @@ def task(get_task): return get_task() -@pytest.fixture -def get_persistent_org_config(persistent_org_config): - return persistent_org_config - - @pytest.fixture def mock_download_extract_github(): with mock.patch( @@ -158,15 +154,16 @@ def mock_get_static_dependencies(): class TestPackageConfig: - def test_org_config(self, project_config, persistent_org_config): + def test_org_config(self, project_config, org_config): + org_config.config_file=None with pytest.raises( TaskOptionsError, - match="Org doesn't contain a config_file. It's a persistent org like Devhub or Developer Edition org", + match=PERSISTANT_ORG_ERROR, ): task = CreatePackageVersion( project_config, TaskConfig(), - persistent_org_config, + org_config ) task.__init__ From d59187be8fe09e1a14c7543dcfe04f81aa7077e3 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Wed, 25 Oct 2023 12:33:35 +0530 Subject: [PATCH 60/98] changed error message & added task_docs --- cumulusci/tasks/create_package_version.py | 4 +++- cumulusci/tasks/tests/test_create_package_version.py | 12 ++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index 019cb8a9fe..8c6ed49157 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -41,10 +41,11 @@ format_subscriber_package_version_where_clause, ) -PERSISTANT_ORG_ERROR=""" +PERSISTANT_ORG_ERROR = """ Target org scratch org definition file missing. Persistent orgs e.g.,Dev Hub can't be used for 2GP package uploads. """ + class PackageTypeEnum(StrEnum): managed = "Managed" unlocked = "Unlocked" @@ -86,6 +87,7 @@ class CreatePackageVersion(BaseSalesforceApiTask): If a package named ``package_name`` does not yet exist in the Dev Hub, it will be created. """ + task_docs = """ Facilitates the upload of 2GP (second-generation packaging) package versions using CumulusCI. diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index 39a3b71d26..b0c36160fd 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -12,7 +12,7 @@ import yaml from pydantic import ValidationError -from cumulusci.core.config import BaseProjectConfig, OrgConfig, TaskConfig, UniversalConfig +from cumulusci.core.config import BaseProjectConfig, TaskConfig, UniversalConfig from cumulusci.core.dependencies.dependencies import ( PackageNamespaceVersionDependency, PackageVersionIdDependency, @@ -28,11 +28,11 @@ from cumulusci.core.keychain import BaseProjectKeychain from cumulusci.salesforce_api.package_zip import BasePackageZipBuilder from cumulusci.tasks.create_package_version import ( + PERSISTANT_ORG_ERROR, CreatePackageVersion, PackageConfig, PackageTypeEnum, VersionTypeEnum, - PERSISTANT_ORG_ERROR, ) from cumulusci.utils import temporary_dir, touch @@ -155,16 +155,12 @@ def mock_get_static_dependencies(): class TestPackageConfig: def test_org_config(self, project_config, org_config): - org_config.config_file=None + org_config.config_file = None with pytest.raises( TaskOptionsError, match=PERSISTANT_ORG_ERROR, ): - task = CreatePackageVersion( - project_config, - TaskConfig(), - org_config - ) + task = CreatePackageVersion(project_config, TaskConfig(), org_config) task.__init__ def test_validate_org_dependent(self): From fe7f56476e6235083d372841c69298b6c6300174 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:30:06 +0530 Subject: [PATCH 61/98] Fix broken links in documentation (#3687) [W-14157597](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001asUQrYAM/view) Fixed broken links in [documentation](https://cumulusci.readthedocs.io/en/stable/cli.html#access-and-manage-orgs) **Line:** To learn about working with orgs in detail, read (scratch-orgs) and (connected-orgs). --- docs/cli.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index b4f00dee01..383928ab20 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -342,8 +342,8 @@ CumulusCI makes it easy to create, connect, and manage orgs. The `cci org` top-level command helps you work with orgs. To learn about working with orgs in detail, read -(scratch-orgs) and -(connected-orgs). +[](scratch-orgs) and +[](connected-orgs). (manage-services)= From 8328bfb9dc799c3195791b813531a7e66a3fd7cb Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 26 Oct 2023 15:15:59 -0700 Subject: [PATCH 62/98] Apply suggestions from code review --- cumulusci/tasks/create_package_version.py | 6 +++--- cumulusci/tasks/tests/test_create_package_version.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index 8c6ed49157..b4d5a264a4 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -41,8 +41,8 @@ format_subscriber_package_version_where_clause, ) -PERSISTANT_ORG_ERROR = """ -Target org scratch org definition file missing. Persistent orgs e.g.,Dev Hub can't be used for 2GP package uploads. +PERSISTENT_ORG_ERROR = """ +Target org scratch org definition file missing. Persistent orgs like a Dev Hub can't be used for 2GP package uploads. """ @@ -158,7 +158,7 @@ def _init_options(self, kwargs): super()._init_options(kwargs) if not self.org_config.config_file: - raise TaskOptionsError(PERSISTANT_ORG_ERROR) + raise TaskOptionsError(PERSISTENT_ORG_ERROR) # Allow these fields to be explicitly set to blanks # so that unlocked builds can override an otherwise-configured diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index b0c36160fd..9be9da36d5 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -28,7 +28,7 @@ from cumulusci.core.keychain import BaseProjectKeychain from cumulusci.salesforce_api.package_zip import BasePackageZipBuilder from cumulusci.tasks.create_package_version import ( - PERSISTANT_ORG_ERROR, + PERSISTENT_ORG_ERROR, CreatePackageVersion, PackageConfig, PackageTypeEnum, @@ -161,7 +161,6 @@ def test_org_config(self, project_config, org_config): match=PERSISTANT_ORG_ERROR, ): task = CreatePackageVersion(project_config, TaskConfig(), org_config) - task.__init__ def test_validate_org_dependent(self): with pytest.raises(ValidationError, match="Only unlocked packages"): From 5c1a4563ebbd7cfddc0cd5357510d46ddaef1d0e Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Fri, 27 Oct 2023 10:13:17 +0530 Subject: [PATCH 63/98] Spelliing mistake changed --- cumulusci/tasks/tests/test_create_package_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index 9be9da36d5..32057828f3 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -158,9 +158,9 @@ def test_org_config(self, project_config, org_config): org_config.config_file = None with pytest.raises( TaskOptionsError, - match=PERSISTANT_ORG_ERROR, + match=PERSISTENT_ORG_ERROR, ): - task = CreatePackageVersion(project_config, TaskConfig(), org_config) + CreatePackageVersion(project_config, TaskConfig(), org_config) def test_validate_org_dependent(self): with pytest.raises(ValidationError, match="Only unlocked packages"): From ffaa93fa5185275afd137c3e36eeffffeb28a8bc Mon Sep 17 00:00:00 2001 From: jain-naman-sf Date: Tue, 31 Oct 2023 11:30:31 +0530 Subject: [PATCH 64/98] Integration Test Fixtures --- cumulusci/cli/org.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulusci/cli/org.py b/cumulusci/cli/org.py index e26df3d09e..3d2d08fc7a 100644 --- a/cumulusci/cli/org.py +++ b/cumulusci/cli/org.py @@ -569,7 +569,7 @@ def org_remove(runtime, org_name, global_org): ) @pass_runtime(require_keychain=True) def org_scratch( - runtime, config_name, org_name, default, devhub, days, no_password, release + runtime, config_name, org_name, default, devhub, days, no_password, release=None ): runtime.check_org_overwrite(org_name) release_options = ["previous", "preview"] From cfec99ded9e74e203406adba58188d2f11efa045 Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Thu, 2 Nov 2023 17:38:22 +0530 Subject: [PATCH 65/98] Feature/add collision-check option to deploy task (#3667) Hey @jkasturi-sf Raising it for review. --------- Co-authored-by: Grandhi Sai Venkata Mahesh Co-authored-by: Jaipal Reddy Kasturi --- cumulusci/tasks/salesforce/Deploy.py | 110 +++++++++-- .../tasks/salesforce/tests/test_Deploy.py | 180 ++++++++++++++++++ .../salesforce/tests/test_DeployBundles.py | 3 + 3 files changed, 278 insertions(+), 15 deletions(-) diff --git a/cumulusci/tasks/salesforce/Deploy.py b/cumulusci/tasks/salesforce/Deploy.py index 4f42a8a14d..bf27b86f53 100644 --- a/cumulusci/tasks/salesforce/Deploy.py +++ b/cumulusci/tasks/salesforce/Deploy.py @@ -1,8 +1,10 @@ import pathlib -from typing import List, Optional +from typing import List, Optional, Union +from defusedxml.minidom import parseString from pydantic import ValidationError +from cumulusci.cli.ui import CliTable from cumulusci.core.dependencies.utils import TaskContext from cumulusci.core.exceptions import TaskOptionsError from cumulusci.core.sfdx import convert_sfdx_source @@ -11,16 +13,18 @@ SourceTransformList, ) from cumulusci.core.utils import process_bool_arg, process_list_arg -from cumulusci.salesforce_api.metadata import ApiDeploy +from cumulusci.salesforce_api.metadata import ApiDeploy, ApiRetrieveUnpackaged from cumulusci.salesforce_api.package_zip import MetadataPackageZipBuilder from cumulusci.salesforce_api.rest_deploy import RestDeploy from cumulusci.tasks.salesforce.BaseSalesforceMetadataApiTask import ( BaseSalesforceMetadataApiTask, ) +from cumulusci.utils.xml import metadata_tree class Deploy(BaseSalesforceMetadataApiTask): api_class = ApiDeploy + api_retrieve_unpackaged = ApiRetrieveUnpackaged task_options = { "path": { "description": "The path to the metadata source to be deployed", @@ -38,6 +42,9 @@ class Deploy(BaseSalesforceMetadataApiTask): "check_only": { "description": "If True, performs a test deployment (validation) of components without saving the components in the target org" }, + "collision_check": { + "description": "If True, performs a collision check with metadata already present in the target org" + }, "test_level": { "description": "Specifies which tests are run as part of a deployment. Valid values: NoTestRun, RunLocalTests, RunAllTestsInOrg, RunSpecifiedTests." }, @@ -89,6 +96,8 @@ def _init_options(self, kwargs): self.options.get("namespace_inject") or self.project_config.project__package__namespace ) + if "collision_check" not in self.options: + self.options["collision_check"] = False if "transforms" in self.options: try: @@ -109,7 +118,22 @@ def _get_api(self, path=None): path = self.options.get("path") package_zip = self._get_package_zip(path) - if package_zip is not None: + + if isinstance(package_zip, dict): + self.logger.warning( + "Deploy getting aborted due to collision of following components" + ) + table_header_row = ["Type", "Component API Name"] + table_data = [table_header_row] + for type in package_zip.keys(): + for component_name in package_zip[type]: + table_data.append([type, component_name]) + table = CliTable( + table_data, + ) + table.echo() + return None + elif package_zip is not None: self.logger.info("Payload size: {} bytes".format(len(package_zip))) else: self.logger.warning("Deployment package is empty; skipping deployment.") @@ -138,7 +162,54 @@ def _is_namespaced_org(self, ns: Optional[str]) -> bool: return process_bool_arg(self.options.get("namespaced_org", False)) return bool(ns) and ns == self.org_config.namespace - def _get_package_zip(self, path) -> Optional[str]: + def _create_api_object(self, package_xml, api_version): + api_retrieve_unpackaged_object = self.api_retrieve_unpackaged( + self, package_xml, api_version + ) + return api_retrieve_unpackaged_object + + def _collision_check(self, src_path): + xml_map = {} + is_collision = False + package_xml = open(f"{src_path}/package.xml", "r") + source_xml_tree = metadata_tree.parse(f"{src_path}/package.xml") + + for type in source_xml_tree.types: + members = [] + try: + for member in type.members: + members.append(member.text) + except AttributeError: # Exception if there are no members for a type + pass + xml_map[type["name"].text] = members + + api_retrieve_unpackaged_response = self._create_api_object( + package_xml.read(), source_xml_tree.version.text + ) + + messages = parseString( + api_retrieve_unpackaged_response._get_response().content + ).getElementsByTagName("messages") + + for i in range(len(messages)): + # print(messages[i]) + message_list = messages[ + i + ].firstChild.nextSibling.firstChild.nodeValue.split("'") + + if message_list[3] in xml_map[message_list[1]]: + xml_map[message_list[1]].remove(message_list[3]) + if len(xml_map[message_list[1]]) == 0: + del xml_map[message_list[1]] + + for type, api_names in xml_map.items(): + if len(api_names) != 0: + is_collision = True + break + + return is_collision, xml_map + + def _get_package_zip(self, path) -> Union[str, dict, None]: assert path, f"Path should be specified for {self.__class__.name}" if not pathlib.Path(path).exists(): self.logger.warning(f"{path} not found.") @@ -154,19 +225,28 @@ def _get_package_zip(self, path) -> Optional[str]: "namespaced_org": self._is_namespaced_org(namespace), } package_zip = None + with convert_sfdx_source(path, None, self.logger) as src_path: - context = TaskContext(self.org_config, self.project_config, self.logger) - package_zip = MetadataPackageZipBuilder( - path=src_path, - context=context, - options=options, - transforms=self.transforms, - ) + ############## + is_collision = False + if "collision_check" in options and options["collision_check"]: + is_collision, xml_map = self._collision_check(src_path) + ############# + if not is_collision: + context = TaskContext(self.org_config, self.project_config, self.logger) + package_zip = MetadataPackageZipBuilder( + path=src_path, + context=context, + options=options, + transforms=self.transforms, + ) - # If the package is empty, do nothing. - if not package_zip.zf.namelist(): - return - return package_zip.as_base64() + # If the package is empty, do nothing. + if not package_zip.zf.namelist(): + return + return package_zip.as_base64() + else: + return xml_map def freeze(self, step): steps = super().freeze(step) diff --git a/cumulusci/tasks/salesforce/tests/test_Deploy.py b/cumulusci/tasks/salesforce/tests/test_Deploy.py index d971f3a230..a37f9b33fc 100644 --- a/cumulusci/tasks/salesforce/tests/test_Deploy.py +++ b/cumulusci/tasks/salesforce/tests/test_Deploy.py @@ -2,6 +2,7 @@ import io import os import zipfile +from unittest import mock import pytest @@ -36,6 +37,167 @@ def test_get_api(self, rest_deploy): assert "package.xml" in zf.namelist() zf.close() + def test_create_api_object(self): + with temporary_dir() as path: + task = create_task( + Deploy, + { + "path": path, + "namespace_tokenize": "ns", + "namespace_inject": "ns", + "namespace_strip": "ns", + "unmanaged": True, + "collision_check": True, + }, + ) + package_xml = """ + + + Delivery__c.Supplier__c + Delivery__c.Scheduled_Date__c + CustomField + + + CustomObject + + + CustomTab + + + Layout + + + ListView + + 58.0 + """ + expected_package_xml = """Delivery__c.Supplier__cDelivery__c.Scheduled_Date__cCustomFieldCustomObjectCustomTabLayoutListView58.0 """ + api_object = task._create_api_object(package_xml, "58.0") + + assert api_object.package_xml.split() == expected_package_xml.split() + assert api_object.api_version == "58.0" + + def test_collision_check_positive(self): + with temporary_dir() as path: + touch("package.xml") + with open("package.xml", "w") as f: + f.write( + """ + + + Delivery__c.Supplier__c + Delivery__c.Scheduled_Date__c + CustomField + + + CustomObject + + + CustomTab + + + Layout + + + ListView + + 58.0 + """ + ) + task = create_task( + Deploy, + { + "path": path, + "namespace_tokenize": "ns", + "namespace_inject": "ns", + "namespace_strip": "ns", + "unmanaged": True, + "collision_check": True, + }, + ) + + task._create_api_object = mock.Mock( + return_value=mock.Mock( + _get_response=mock.Mock( + return_value=mock.Mock( + content=' true0051m0000069dpxAAAUser User2023-10-09T08:47:44.875Zunpackaged/package.xmlunpackaged/package.xml0051m0000069dpxAAAUser User2023-10-09T08:47:44.875ZunmanagedPackage09S1m000001EKBkEAOunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery_Item__c.Food_Expiration_Date__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery__c.Status__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery_Item__c.Food_Storage__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery_Item__c.Delivery__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery__c.Scheduled_Date__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery__c.Supplier__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomObject' named 'Delivery__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomObject' named 'Delivery_Item__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomTab' named 'Delivery__c' cannot be foundunpackaged/package.xmlEntity of type 'Layout' named 'Delivery_Item__c-Delivery Item Layout' cannot be foundunpackaged/package.xmlEntity of type 'Layout' named 'Delivery__c-Delivery Layout' cannot be foundunpackaged/package.xmlEntity of type 'ListView' named 'Delivery__c.All' cannot be foundSucceededtrueUEsDBBQACAgIAPZFSVcAAAAAAAAAAAAAAAAWAAAAdW5wYWNrYWdlZC9wYWNrYWdlLnhtbKVTTU/DMAy991dUva8pCNCE0kyIUQkJCaQNrlGami2QNFXjwvrvyT7KuFTraE7Ji5/fsy3T2cbo8Atqp2yZRhdxEoVQSluocpVGr8tsMo1mLKAvQn6KFYQ+unRptEasbglxVlSxe7e1hFhaQy6T5IYkV8QAikKgiFgQ+kOxrcDt77u3AZN7STYHrbx2yx8RDOcy/gW4pKSLGsDLrC34w6ZStUBfCJ8LhH/lWKCtfZ1DuZ62kGsoGg3FWaJbIgps3DmEpqq0grqHUgoD7L5xaE2mQBeU7JD9BMifEQwex0BjJ8w85x8gcYSbkwpLkY8vdtIB4RYIn0RrGxzYgSO3n7bz132P6EV8p3V/fuXwTcF3v8Jh1dn1NE4o6V4BJYcNZ8EPUEsHCGtr2HwiAQAAEwQAAFBLAQIUABQACAgIAPZFSVdra9h8IgEAABMEAAAWAAAAAAAAAAAAAAAAAAAAAAB1bnBhY2thZ2VkL3BhY2thZ2UueG1sUEsFBgAAAAABAAEARAAAAGYBAAAAAA==' + ) + ) + ) + ) + + is_collision, xml_map = task._collision_check(path) + + assert is_collision is False + print(xml_map) + assert xml_map == { + "CustomObject": [], + "CustomTab": [], + "Layout": [], + "ListView": [], + } + + def test_collision_check_negative(self): + + with temporary_dir() as path: + touch("package.xml") + with open("package.xml", "w") as f: + f.write( + """ + + + Delivery__c.Supplier__c + Delivery__c.Scheduled_Date__c + CustomField + + + CustomObject + + + CustomTab + + + Layout + + + ListView + + 58.0 + """ + ) + task = create_task( + Deploy, + { + "path": path, + "namespace_tokenize": "ns", + "namespace_inject": "ns", + "namespace_strip": "ns", + "unmanaged": True, + "collision_check": True, + }, + ) + + task._create_api_object = mock.Mock( + return_value=mock.Mock( + _get_response=mock.Mock( + return_value=mock.Mock( + content=' true0051m0000069dpxAAAUser User2023-10-09T08:47:44.875Zunpackaged/package.xmlunpackaged/package.xml0051m0000069dpxAAAUser User2023-10-09T08:47:44.875ZunmanagedPackage09S1m000001EKBkEAOunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery_Item__c.Food_Expiration_Date__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery__c.Status__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery_Item__c.Food_Storage__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery_Item__c.Delivery__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomField' named 'Delivery__c.Supplier__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomObject' named 'Delivery__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomObject' named 'Delivery_Item__c' cannot be foundunpackaged/package.xmlEntity of type 'CustomTab' named 'Delivery__c' cannot be foundunpackaged/package.xmlEntity of type 'Layout' named 'Delivery_Item__c-Delivery Item Layout' cannot be foundunpackaged/package.xmlEntity of type 'Layout' named 'Delivery__c-Delivery Layout' cannot be foundunpackaged/package.xmlEntity of type 'ListView' named 'Delivery__c.All' cannot be foundSucceededtrueUEsDBBQACAgIAPZFSVcAAAAAAAAAAAAAAAAWAAAAdW5wYWNrYWdlZC9wYWNrYWdlLnhtbKVTTU/DMAy991dUva8pCNCE0kyIUQkJCaQNrlGami2QNFXjwvrvyT7KuFTraE7Ji5/fsy3T2cbo8Atqp2yZRhdxEoVQSluocpVGr8tsMo1mLKAvQn6KFYQ+unRptEasbglxVlSxe7e1hFhaQy6T5IYkV8QAikKgiFgQ+kOxrcDt77u3AZN7STYHrbx2yx8RDOcy/gW4pKSLGsDLrC34w6ZStUBfCJ8LhH/lWKCtfZ1DuZ62kGsoGg3FWaJbIgps3DmEpqq0grqHUgoD7L5xaE2mQBeU7JD9BMifEQwex0BjJ8w85x8gcYSbkwpLkY8vdtIB4RYIn0RrGxzYgSO3n7bz132P6EV8p3V/fuXwTcF3v8Jh1dn1NE4o6V4BJYcNZ8EPUEsHCGtr2HwiAQAAEwQAAFBLAQIUABQACAgIAPZFSVdra9h8IgEAABMEAAAWAAAAAAAAAAAAAAAAAAAAAAB1bnBhY2thZ2VkL3BhY2thZ2UueG1sUEsFBgAAAAABAAEARAAAAGYBAAAAAA==' + ) + ) + ) + ) + + is_collision, xml_map = task._collision_check(path) + + assert is_collision is True + assert xml_map == { + "CustomField": ["Delivery__c.Scheduled_Date__c"], + "CustomObject": [], + "CustomTab": [], + "Layout": [], + "ListView": [], + } + @pytest.mark.parametrize("rest_deploy", [True, False]) def test_get_api__managed(self, rest_deploy): with temporary_dir() as path: @@ -164,6 +326,24 @@ def test_get_api__empty_package_zip(self, rest_deploy): api = task._get_api() assert api is None + def test_get_api__collision_detected(self): + with temporary_dir() as path: + task = create_task( + Deploy, + { + "path": path, + "namespace_tokenize": "ns", + "namespace_inject": "ns", + "namespace_strip": "ns", + "unmanaged": True, + "collision_check": True, + }, + ) + task._get_package_zip = mock.Mock(return_value={"Custom Field": []}) + + api = task._get_api() + assert api is None + @pytest.mark.parametrize("rest_deploy", [True, False]) def test_init_options(self, rest_deploy): with pytest.raises(TaskOptionsError): diff --git a/cumulusci/tasks/salesforce/tests/test_DeployBundles.py b/cumulusci/tasks/salesforce/tests/test_DeployBundles.py index 6ccd8eb129..df94248688 100644 --- a/cumulusci/tasks/salesforce/tests/test_DeployBundles.py +++ b/cumulusci/tasks/salesforce/tests/test_DeployBundles.py @@ -40,7 +40,9 @@ def test_freeze(self): task_class=None, project_config=task.project_config, ) + steps = task.freeze(step) + print(steps) assert [ { "is_required": True, @@ -57,6 +59,7 @@ def test_freeze(self): "ref": task.project_config.repo_commit, "github": "https://github.com/TestOwner/TestRepo", "subfolder": "unpackaged/test", + "collision_check": False, "namespace_inject": None, } ] From 84389d998b4783ddd2ff062f486a2366709cac27 Mon Sep 17 00:00:00 2001 From: lakshmi2506 <141401869+lakshmi2506@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:33:25 +0530 Subject: [PATCH 66/98] Handling exception when the Tooling API returns a test result with a null method name (#3681) Handled the situation when the Tooling API returns a test result with a null methodname by detecting the null and enqueueing all methods in the test class with the given `ApexClassId` for retry. Retry is done for the classes with methodname None in both test case pass and fail conditions and the number of retried tests is updated accordingly. Work item: [link](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00000IMA9gYAH/view) --- cumulusci/tasks/apex/testrunner.py | 16 +++++++ cumulusci/tasks/apex/tests/test_apex_tasks.py | 44 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/cumulusci/tasks/apex/testrunner.py b/cumulusci/tasks/apex/testrunner.py index 3ba6ef1783..468f3d73ac 100644 --- a/cumulusci/tasks/apex/testrunner.py +++ b/cumulusci/tasks/apex/testrunner.py @@ -487,6 +487,22 @@ def _process_test_results(self): class_names = list(self.results_by_class_name.keys()) class_names.sort() for class_name in class_names: + self.retry_details = {} + method_names = list(self.results_by_class_name[class_name].keys()) + # Added to process for the None methodnames + + if None in method_names: + class_id = self.classes_by_name[class_name] + self.retry_details.setdefault(class_id, []).append( + self._get_test_methods_for_class(class_name) + ) + del self.results_by_class_name[class_name][None] + self.logger.info( + f"Retrying class with id: {class_id} name:{class_name} due to `None` methodname" + ) + self.counts["Retriable"] += len(self.retry_details[class_id]) + self._attempt_retries() + has_failures = any( result["Outcome"] in ["Fail", "CompileFail"] for result in self.results_by_class_name[class_name].values() diff --git a/cumulusci/tasks/apex/tests/test_apex_tasks.py b/cumulusci/tasks/apex/tests/test_apex_tasks.py index ad2a3651ef..2d312a8f48 100644 --- a/cumulusci/tasks/apex/tests/test_apex_tasks.py +++ b/cumulusci/tasks/apex/tests/test_apex_tasks.py @@ -176,12 +176,16 @@ def _get_mock_testqueueitem_status_query_url(self, job_id): ) def _mock_get_test_results( - self, outcome="Pass", message="Test Passed", job_id="JOB_ID1234567" + self, + outcome="Pass", + message="Test Passed", + job_id="JOB_ID1234567", + methodname=["TestMethod"], ): url = self._get_mock_test_query_url(job_id) expected_response = self._get_mock_test_query_results( - ["TestMethod"], [outcome], [message] + methodname, [outcome], [message] ) responses.add( responses.GET, url, match_querystring=True, json=expected_response @@ -310,6 +314,42 @@ def test_run_task(self): task() assert len(responses.calls) == 5 + @responses.activate + def test_run_task_None_methodname_fail(self): + self._mock_apex_class_query() + self._mock_run_tests() + self._mock_get_failed_test_classes_failure() + self._mock_tests_complete() + self._mock_get_symboltable() + self._mock_get_test_results(methodname=[None], outcome="Fail") + self._mock_get_test_results(methodname=["test1"]) + task = RunApexTests(self.project_config, self.task_config, self.org_config) + task() + assert task.counts["Retriable"] == 1 + log = self._task_log_handler.messages + assert ( + "Retrying class with id: 1 name:TestClass_TEST due to `None` methodname" + in log["info"] + ) + + @responses.activate + def test_run_task_None_methodname_pass(self): + self._mock_apex_class_query() + self._mock_run_tests() + self._mock_get_failed_test_classes() + self._mock_tests_complete() + self._mock_get_symboltable() + self._mock_get_test_results(methodname=[None]) + self._mock_get_test_results(methodname=["test1"]) + task = RunApexTests(self.project_config, self.task_config, self.org_config) + task() + assert task.counts["Retriable"] == 1 + log = self._task_log_handler.messages + assert ( + "Retrying class with id: 1 name:TestClass_TEST due to `None` methodname" + in log["info"] + ) + @responses.activate def test_run_task__server_error(self): self._mock_apex_class_query() From 86e2d5a8aac5948842e9fd3a5f638b4c72cc1514 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:11:57 -0700 Subject: [PATCH 67/98] Release v3.81.0 (#3696) Co-authored-by: github-actions Co-authored-by: James Estevez --- cumulusci/__about__.py | 2 +- docs/history.md | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 32fd96398d..58dff8fea3 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.80.0" +__version__ = "3.81.0" diff --git a/docs/history.md b/docs/history.md index 9cfa1bf2cf..1aa01d973c 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,27 @@ +## v3.81.0 (2023-11-03) + + + +## What's Changed + +### Changes 🎉 + +- Add option to specify release window in `cci org scratch` by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3653](https://github.com/SFDO-Tooling/CumulusCI/pull/3653) +- Add describe_metadatatypes task by [@lakshmi2506](https://github.com/lakshmi2506) in [#3669](https://github.com/SFDO-Tooling/CumulusCI/pull/3669) +- Fix JSON output for `cci org info` by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3675](https://github.com/SFDO-Tooling/CumulusCI/pull/3675) +- Fix `TypeError` when service sensitive attribute is `None` by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3674](https://github.com/SFDO-Tooling/CumulusCI/pull/3674) +- metadeploy_publish presents clear errors when plans are not available by [@lakshmi2506](https://github.com/lakshmi2506) in [#3684](https://github.com/SFDO-Tooling/CumulusCI/pull/3684) +- Add clear error message when `create_package_version` run against persistent orgs by [@lakshmi2506](https://github.com/lakshmi2506) in [#3676](https://github.com/SFDO-Tooling/CumulusCI/pull/3676) +- Add collision-check option to deploy task by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3667](https://github.com/SFDO-Tooling/CumulusCI/pull/3667) +- Handling exception when the Tooling API returns a test result with a null method name by [@lakshmi2506](https://github.com/lakshmi2506) in [#3681](https://github.com/SFDO-Tooling/CumulusCI/pull/3681) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.80.0...v3.81.0 + + + ## v3.80.0 (2023-09-29) @@ -37,8 +58,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.79.0...v3.80.0 - - ## v3.79.0 (2023-09-07) From 86ddebbf3264d489b41829b0eca1e353015df194 Mon Sep 17 00:00:00 2001 From: lakshmi2506 <141401869+lakshmi2506@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:52:01 +0530 Subject: [PATCH 68/98] Error message when git remote URL throws exception if no origin remote (#3679) Fails to give the error message when run any task involving git if the URL doesn't contain the origin remote. This is because in the project config URL is set to None due to this`url = self.git_config_remote_origin_url()` and when the below function is called. Parse_repo_url is called with None as argument. `def get_github_api_for_repo(keychain, repo_url, session=None) -> GitHub: owner, repo_name, host = parse_repo_url(repo_url)` Work item: [link](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001M62egYAB/view) --- cumulusci/utils/git.py | 7 +++++++ cumulusci/utils/tests/test_git.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/cumulusci/utils/git.py b/cumulusci/utils/git.py index 2e822bf7d6..a540e22d36 100644 --- a/cumulusci/utils/git.py +++ b/cumulusci/utils/git.py @@ -2,6 +2,10 @@ import re from typing import Any, Optional, Tuple +EMPTY_URL_MESSAGE = """ +The provided URL is empty or no URL under git remote "origin". +""" + def git_path(repo_root: str, tail: Any = None) -> Optional[pathlib.Path]: """Returns a Path to the .git directory in repo_root @@ -71,6 +75,9 @@ def parse_repo_url(url: str) -> Tuple[str, str, str]: Tuple: (str, str, str) Returns (owner, name, host) """ + if not url: + raise ValueError(EMPTY_URL_MESSAGE) + url_parts = re.split("/|@|:", url.rstrip("/")) name = url_parts[-1] diff --git a/cumulusci/utils/tests/test_git.py b/cumulusci/utils/tests/test_git.py index f7cf56eb52..7496486a1b 100644 --- a/cumulusci/utils/tests/test_git.py +++ b/cumulusci/utils/tests/test_git.py @@ -1,6 +1,7 @@ import pytest from cumulusci.utils.git import ( + EMPTY_URL_MESSAGE, construct_release_branch_name, get_release_identifier, is_release_branch, @@ -69,3 +70,9 @@ def test_construct_release_branch_name(): def test_parse_repo_url(repo_uri, owner, repo_name, host): assert parse_repo_url(repo_uri) == (owner, repo_name, host) assert split_repo_url(repo_uri) == (owner, repo_name) + + +@pytest.mark.parametrize("URL", [None, ""]) +def test_empty_url(URL): + with pytest.raises(ValueError, match=EMPTY_URL_MESSAGE): + parse_repo_url(URL) From 609a46ba9a2ec23e975ea9f6762cc9385d23162c Mon Sep 17 00:00:00 2001 From: lakshmi2506 <141401869+lakshmi2506@users.noreply.github.com> Date: Mon, 6 Nov 2023 17:27:05 +0530 Subject: [PATCH 69/98] Improve task return_values documentation (#3689) Documentation on how we can reference the return values of the prior tasks for task options list or dictionary. --------- Co-authored-by: Jaipal Reddy Kasturi --- docs/config.md | 62 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/docs/config.md b/docs/config.md index 0435bb4056..12e3db1e8e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -771,41 +771,67 @@ method](https://github.com/SFDO-Tooling/CumulusCI/blob/3cad07ac1cecf438aaf087cde sets `self.return_values` to a dictionary with these keys: `version_number`, `version_id`, and `package_id`. -Now look at the standard `release_beta` flow defined in the universal +Now look at the standard `release_unlocked_production` flow defined in the universal `cumulusci.yml` file: ```yaml -release_beta: - description: Upload and release a beta version of the metadata currently in packaging +release_unlocked_production: + group: Release Operations + description: Promote the latest beta 2GP unlocked package version and create a new release in GitHub steps: 1: - task: upload_beta - options: - name: Automated beta release + task: promote_package_version 2: task: github_release options: - version: ^^upload_beta.version_number + version: ^^promote_package_version.version_number + version_id: ^^promote_package_version.version_id + dependencies: ^^promote_package_version.dependencies + package_type: 2GP + tag_prefix: $project_config.project__git__prefix_release 3: task: github_release_notes - ignore_failure: True ## Attempt to generate release notes but don't fail build + ignore_failure: True options: - link_pr: True publish: True tag: ^^github_release.tag_name - include_empty: True - version_id: ^^upload_beta.version_id - 4: - task: github_master_to_feature + version_id: ^^promote_package_version.version_id ``` This flow shows how subsequent tasks can reference the return values of a prior task. In this case, the `github_release` task uses the -`version_numer` set by the `upload_beta` task as an option value with -the `^^upload_beta.version_number` syntax. Similarly, the -`github_release_notes` task uses the `version_id` set by the -`upload_beta` task as an option value with the -`^^upload_beta.version_id` syntax. +`version_numer` set by the `promote_package_version` task as an option value +with the `^^promote_package_version.version_number` syntax. Here, `dependencies` +is of type list and it uses the list from `promote_package_version` task as an +option value with `^^promote_package_version.dependencies` syntax. + +Similarly, the `github_release_notes` task uses the `version_id` set by the +`promote_package_version` task as an option value with the +`^^promote_package_version.version_number` syntax and uses the `tag` set by +`github_release` task as an option value with the `^^github_release.tag_name` +syntax. + +The below `example_flow` shows how the task options of type list CANNOT be used. +Here, `update_dependencies` task does not set the task option `dependencies` +as the list of values from the prior tasks. Similarly, task options of type +dictionary cannot be set as key value pairs from the prior tasks. + +```yaml +example_flow: + description: You cannot make a list/dict with return values like below + steps: + 1: + task: get_latest_version_example + 2: + task: get_old_version_example + 3: + task: update_dependencies + options: + dependencies: + - latest_version_id: ^^get_latest_version_example.version_id + - version_id: ^^get_old_version_example.version_id + packages_only: true +``` ## Troubleshoot Configurations From 630eee734e874cee4cdbf17f4ff19cedda7f2e3e Mon Sep 17 00:00:00 2001 From: Grandhi Sai Venkata Mahesh Date: Mon, 6 Nov 2023 20:19:30 +0530 Subject: [PATCH 70/98] Fix Github url parse error for some scenarios (#3683) Fix Github parse issue during some scenarios as mentioned below 1. https://user@github.com/owner/repo.git 2. git@github.com:/SalesforceFoundation/Subledger.git --- cumulusci/utils/git.py | 9 ++++++++- cumulusci/utils/tests/test_git.py | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cumulusci/utils/git.py b/cumulusci/utils/git.py index a540e22d36..cc4b7c7f86 100644 --- a/cumulusci/utils/git.py +++ b/cumulusci/utils/git.py @@ -79,6 +79,7 @@ def parse_repo_url(url: str) -> Tuple[str, str, str]: raise ValueError(EMPTY_URL_MESSAGE) url_parts = re.split("/|@|:", url.rstrip("/")) + url_parts = list(filter(None, url_parts)) name = url_parts[-1] if name.endswith(".git"): @@ -87,7 +88,13 @@ def parse_repo_url(url: str) -> Tuple[str, str, str]: owner = url_parts[-2] host = url_parts[-3] + # Regular Expression to match domain of host com,org,in,app etc + domain_search_exp = re.compile(r"\.[a-zA-Z]+$") # Need to consider "https://api.github.com/repos/owner/repo/" pattern - if "http" in url_parts[0] and len(url_parts) > 6: + if ( + "http" in url_parts[0] + and len(url_parts) > 4 + and domain_search_exp.search(host) is None + ): host = url_parts[-4] return (owner, name, host) diff --git a/cumulusci/utils/tests/test_git.py b/cumulusci/utils/tests/test_git.py index 7496486a1b..93b1937a1c 100644 --- a/cumulusci/utils/tests/test_git.py +++ b/cumulusci/utils/tests/test_git.py @@ -45,6 +45,12 @@ def test_construct_release_branch_name(): ), ("https://github.com/owner/repo_name/", "owner", "repo_name", "github.com"), ("https://github.com/owner/repo_name.git", "owner", "repo_name", "github.com"), + ( + "https://user@github.com/owner/repo_name.git", + "owner", + "repo_name", + "github.com", + ), ( "https://git.ent.example.com/org/private_repo.git", "org", @@ -52,6 +58,7 @@ def test_construct_release_branch_name(): "git.ent.example.com", ), ("git@github.com:owner/repo_name.git", "owner", "repo_name", "github.com"), + ("git@github.com:/owner/repo_name.git", "owner", "repo_name", "github.com"), ("git@github.com:owner/repo_name", "owner", "repo_name", "github.com"), ( "git@api.github.com/owner/repo_name/", From a50a072cbfada1a6fa0c2684b89666745fc049a1 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:35:49 +0530 Subject: [PATCH 71/98] Task to retrieve a complete Profile from an org (#3672) [W-8932343](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07B00000097PhOIAU/view) - Build a task to retrieve a complete Profile from an org Permissionable Entities Retrieved: 1. CustomApplication 2. ApexClass 3. CustomPermission 4. CustomObject 5. ExternalDataSource 6. Flow 7. ApexPage 8. CustomTab Issues: 1. I'm able to retrieve flows within the same package.xml, but they are not visible under the Profile metadata. **(Added functionality to explicitly add them to the profile-meta.xml file)** 2. While retrieving apps, those without a StartUrl in the AppMenuItem table are not being included in the retrieval process. This seems to be a package.xml issue, not something within our control. 3. Two sObjects, AIRecordInsight and Document, are not being successfully retrieved even when specified in the package.xml. **(Edit: However, they show up under the ObjectPermissions in the profile-meta.xml file, so no issues)** --------- Co-authored-by: David Reed --- cumulusci/cumulusci.yml | 4 + .../salesforce_api/retrieve_profile_api.py | 295 +++++++++++++++ .../tests/test_retrieve_profile_api.py | 335 ++++++++++++++++++ .../tasks/salesforce/retrieve_profile.py | 172 +++++++++ .../salesforce/tests/test_retrieve_profile.py | 253 +++++++++++++ .../run_queries_in_parallel.py | 29 ++ .../tests/test_run_queries_in_parallel.py | 50 +++ 7 files changed, 1138 insertions(+) create mode 100644 cumulusci/salesforce_api/retrieve_profile_api.py create mode 100644 cumulusci/salesforce_api/tests/test_retrieve_profile_api.py create mode 100644 cumulusci/tasks/salesforce/retrieve_profile.py create mode 100644 cumulusci/tasks/salesforce/tests/test_retrieve_profile.py create mode 100644 cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py create mode 100644 cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 6a3bf98422..301fe6adf6 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -200,6 +200,10 @@ tasks: options: license: Salesforce group: Salesforce Metadata + retrieve_profile: + description: Given a list of profiles, the task retrieves all complete profiles along with their associated dependencies for all permissionable entities - ApexClass, ApexPage, CustomApplications, CustomObjects, CustomPermissions, CustomTabs, ExternalDataSources and Flows + class_path: cumulusci.tasks.salesforce.retrieve_profile.RetrieveProfile + group: Salesforce Metadata delete_data: description: Query existing data for a specific sObject and perform a Bulk API delete of all matching records. class_path: cumulusci.tasks.bulkdata.DeleteData diff --git a/cumulusci/salesforce_api/retrieve_profile_api.py b/cumulusci/salesforce_api/retrieve_profile_api.py new file mode 100644 index 0000000000..4b100ae542 --- /dev/null +++ b/cumulusci/salesforce_api/retrieve_profile_api.py @@ -0,0 +1,295 @@ +import json +from typing import Dict, List, Union + +import salesforce_bulk +from requests.exceptions import ConnectionError +from simple_salesforce.exceptions import SalesforceGeneralError + +from cumulusci.tasks.salesforce.BaseSalesforceApiTask import BaseSalesforceApiTask +from cumulusci.utils.parallel.queries_in_parallel.run_queries_in_parallel import ( + RunParallelQueries, +) + + +class MetadataInfo: + def __init__( + self, columns: List[str], table_name: str, package_xml_name: str, id_field: str + ): + self.columns = columns + self.table_name = table_name + self.package_xml_name = package_xml_name + self.id_field = id_field + + +APEXCLASS = MetadataInfo( + columns=["Name", "NamespacePrefix"], + table_name="ApexClass", + package_xml_name="ApexClass", + id_field="Id", +) +APEXPAGE = MetadataInfo( + columns=["Name", "NamespacePrefix"], + table_name="ApexPage", + package_xml_name="ApexPage", + id_field="Id", +) +CUSTOMPERMISSION = MetadataInfo( + columns=["DeveloperName", "NamespacePrefix"], + table_name="CustomPermission", + package_xml_name="CustomPermission", + id_field="Id", +) +TABSET = MetadataInfo( + columns=["Name", "NamespacePrefix"], + table_name="AppMenuItem", + package_xml_name="CustomApplication", + id_field="ApplicationId", +) +CONNECTEDAPPLICATION = MetadataInfo( + columns=["Name", "NamespacePrefix"], + table_name="AppMenuItem", + package_xml_name="CustomApplication", + id_field="ApplicationId", +) +EXTERNALDATASOURCE = MetadataInfo( + columns=["DeveloperName", "NamespacePrefix"], + table_name="ExternalDataSource", + package_xml_name="ExternalDataSource", + id_field="Id", +) +FLOWDEFINITION = MetadataInfo( + columns=["ApiName"], + table_name="FlowDefinitionView", + package_xml_name="Flow", + id_field="Id", +) + +SETUP_ENTITY_TYPES = { + "ApexClass": APEXCLASS, + "ApexPage": APEXPAGE, + "CustomPermission": CUSTOMPERMISSION, + "TabSet": TABSET, + "ConnectedApplication": CONNECTEDAPPLICATION, + "ExternalDataSource": EXTERNALDATASOURCE, + "FlowDefinition": FLOWDEFINITION, +} +SETUP_ENTITY_QUERY_NAME = "setupEntityAccess" + +SOBJECT_TYPE = "CustomObject" +SOBJECT_QUERY_NAME = "sObject" + +CUSTOM_TAB_TYPE = "CustomTab" +CUSTOM_TAB_QUERY_NAME = "customTab" + +PROFILE_FLOW_QUERY_NAME = "profileFlow" + + +class RetrieveProfileApi(BaseSalesforceApiTask): + def _init_task(self): + super(RetrieveProfileApi, self)._init_task() + self.api_version = self.org_config.latest_api_version + + def _retrieve_existing_profiles(self, profiles: List[str]): + query = self._build_query(["Name"], "Profile", {"Name": profiles}) + result = self._run_query(query) + + existing_profiles = [] + for data in result["records"]: + existing_profiles.append(data["Name"]) + + return existing_profiles + + def _run_query(self, query): + try: + result = self.sf.query(query) + except (SalesforceGeneralError, ConnectionError): + result = self._run_bulk_query(query) + return result + + def _run_bulk_query(self, query): + table_name = self._extract_table_name_from_query(query) + job = self.bulk.create_query_job(table_name, contentType="JSON") + + batch = self.bulk.query(job, query) + self.bulk.wait_for_batch(job, batch) + self.bulk.close_job(job) + results = self.bulk.get_all_results_for_query_batch(batch) + for result in results: + result = json.load(salesforce_bulk.util.IteratorBytesIO(result)) + return {"records": result} + + def _extract_table_name_from_query(self, query): + from_index = query.upper().find("FROM") + if from_index != -1: + query = query[from_index + len("FROM") :].strip() + table_name = query.split()[0] + return table_name + else: + raise ValueError("Invalid query format. 'FROM' clause not found.") + + def _build_query( + self, + columns: List[str], + table_name: str, + where: Dict[str, Union[str, List[str]]] = None, + ) -> str: + if not columns: + raise ValueError("Columns list cannot be empty") + if not table_name: + raise ValueError("Table name cannot be empty") + + select_clause = ", ".join(columns) + + where_clause = "" + if where: + where_list = [] + for key, value in where.items(): + if isinstance(value, list): + value = ", ".join([f"'{item}'" for item in value]) + condition = f"{key} IN ({value})" + else: + condition = f"{key} = '{value}'" + where_list.append(condition) + where_clause = "WHERE " + " AND ".join(where_list) + + query = f"SELECT {select_clause} FROM {table_name} {where_clause}".strip() + return query + + def _queries_retrieve_permissions(self, profiles: List[str]): + queries = {} + + # Setup Entity Access query + setupEntityAccess_query = self._build_query( + ["SetupEntityId", "SetupEntityType"], + "SetupEntityAccess", + {"Parent.Profile.Name": profiles}, + ) + queries[SETUP_ENTITY_QUERY_NAME] = setupEntityAccess_query + + # sObject query + sObject_query = self._build_query( + ["SObjectType"], "ObjectPermissions", {"Parent.Profile.Name": profiles} + ) + queries[SOBJECT_QUERY_NAME] = sObject_query + + # Custom Tab query + customTab_query = self._build_query( + ["Name"], "PermissionSetTabSetting", {"Parent.Profile.Name": profiles} + ) + queries[CUSTOM_TAB_QUERY_NAME] = customTab_query + + # Matching Profile Name and Flow query + profileFlow_query = self._build_query( + ["SetupEntityId", "Parent.Profile.Name"], + "SetupEntityAccess", + {"Parent.Profile.Name": profiles, "SetupEntityType": "FlowDefinition"}, + ) + queries[PROFILE_FLOW_QUERY_NAME] = profileFlow_query + + return queries + + def _process_all_results(self, result: dict): + permissionable_entities = {} + entities, result_setupEntityAccess = self._process_setupEntityAccess_results( + result[SETUP_ENTITY_QUERY_NAME] + ) + permissionable_entities.update(entities) + permissionable_entities.update( + self._process_sObject_results(result[SOBJECT_QUERY_NAME]) + ) + permissionable_entities.update( + self._process_customTab_results(result[CUSTOM_TAB_QUERY_NAME]) + ) + + # Process the profile and flows + profile_flow = self._match_profiles_and_flows( + result[PROFILE_FLOW_QUERY_NAME], + result_setupEntityAccess.get("FlowDefinition", []), + ) + + return permissionable_entities, profile_flow + + # Retrieve all the permissionable entitites for a set of profiles + def _retrieve_permissionable_entities(self, profiles: List[str]): + # Logs + self.logger.info("Querying for all permissionable entities:") + self.logger.info("Pending") + + # Run all queries + result = RunParallelQueries._run_queries_in_parallel( + self._queries_retrieve_permissions(profiles), self._run_query + ) + + # Process the results + permissionable_entities, profile_flow = self._process_all_results(result) + + # Logs + self.logger.info("[Done]\n") + + return permissionable_entities, profile_flow + + def _process_setupEntityAccess_results(self, result_list: List[dict]): + setupEntityAccess_dict = { + entity_type: [] for entity_type in SETUP_ENTITY_TYPES.keys() + } + + for data in result_list: + entity_type = data["SetupEntityType"] + entity_id = data["SetupEntityId"] + if entity_type in SETUP_ENTITY_TYPES.keys(): + setupEntityAccess_dict[entity_type].append(entity_id) + + queries = {} + for entity_type, query_values in SETUP_ENTITY_TYPES.items(): + if query_values and len(setupEntityAccess_dict[entity_type]) != 0: + queries[entity_type] = self._build_query( + query_values.columns, + query_values.table_name, + {query_values.id_field: setupEntityAccess_dict[entity_type]}, + ) + + result = RunParallelQueries._run_queries_in_parallel(queries, self._run_query) + + extracted_values = {} + for entity_type, data in SETUP_ENTITY_TYPES.items(): + if entity_type in result and data: + extracted_values.setdefault(data.package_xml_name, []) + for item in result[entity_type]: + if ( + "NamespacePrefix" in item + and item["NamespacePrefix"] is not None + ): + extracted_values[data.package_xml_name].append( + f'{item["NamespacePrefix"]}__{item[data.columns[0]]}' + ) + else: + extracted_values[data.package_xml_name].append( + item[data.columns[0]] + ) + + return extracted_values, result + + def _process_sObject_results(self, result_list: List[dict]): + sObject_list = [data["SobjectType"] for data in result_list] + return {SOBJECT_TYPE: sObject_list} + + def _process_customTab_results(self, result_list: List[dict]): + customTab_list = [data["Name"] for data in result_list] + return {CUSTOM_TAB_TYPE: customTab_list} + + def _match_profiles_and_flows( + self, result_profiles: List[dict], result_flows: List[dict] + ): + profile_mapping = {} + for item in result_profiles: + setup_entity_id = item["SetupEntityId"] + profile_name = item["Parent"]["Profile"]["Name"] + profile_mapping.setdefault(setup_entity_id, []).append(profile_name) + + result_dict = {} + for flow in result_flows: + setup_entity_id = flow["attributes"]["url"].split("/")[-1] + for profile_name in profile_mapping.get(setup_entity_id, []): + result_dict.setdefault(profile_name, []).append(flow["ApiName"]) + + return result_dict diff --git a/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py b/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py new file mode 100644 index 0000000000..177e1b1b09 --- /dev/null +++ b/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py @@ -0,0 +1,335 @@ +from io import StringIO +from unittest.mock import MagicMock, patch + +import pytest +from requests.exceptions import ConnectionError + +from cumulusci.salesforce_api.retrieve_profile_api import RetrieveProfileApi +from cumulusci.tasks.salesforce.BaseSalesforceApiTask import BaseSalesforceApiTask +from cumulusci.utils.parallel.queries_in_parallel.run_queries_in_parallel import ( + RunParallelQueries, +) + + +@pytest.fixture +def retrieve_profile_api_instance(): + sf_mock = MagicMock() + bulk_mock = MagicMock() + project_config = MagicMock() + task_config = MagicMock() + org_config = MagicMock() + org_config.latest_api_version = "58.0" + sf_mock.query.return_value = {"records": []} + api = RetrieveProfileApi( + project_config=project_config, org_config=org_config, task_config=task_config + ) + api.sf = sf_mock + api.bulk = bulk_mock + return api + + +def test_init_task(retrieve_profile_api_instance): + with patch.object(BaseSalesforceApiTask, "_init_task"): + retrieve_profile_api_instance._init_task() + + assert retrieve_profile_api_instance.api_version == "58.0" + + +def test_retrieve_existing_profiles(retrieve_profile_api_instance): + profiles = ["Profile1", "Profile2"] + result = {"records": [{"Name": "Profile1"}]} + with patch.object( + RetrieveProfileApi, "_build_query", return_value="some_query" + ), patch.object(RetrieveProfileApi, "_run_query", return_value=result): + existing_profiles = retrieve_profile_api_instance._retrieve_existing_profiles( + profiles + ) + + assert "Profile1" in existing_profiles + assert "Profile2" not in existing_profiles + + +def test_run_query_sf(retrieve_profile_api_instance): + query = "SELECT Id FROM Account" + result_data = {"records": [{"Id": "001abc"}]} + retrieve_profile_api_instance.sf.query.return_value = result_data + + result = retrieve_profile_api_instance._run_query(query) + assert result == result_data + + +def test_run_query_bulk(retrieve_profile_api_instance): + query = "SELECT Id FROM Account" + result_data = {"records": [{"Id": "001abc"}]} + retrieve_profile_api_instance.sf.query.side_effect = ConnectionError + + retrieve_profile_api_instance.bulk.create_query_job.return_value = "job_id" + retrieve_profile_api_instance.bulk.query.return_value = "batch_id" + retrieve_profile_api_instance.bulk.wait_for_batch.return_value = None + retrieve_profile_api_instance.bulk.get_all_results_for_query_batch.return_value = [ + "some_value" + ] + + with patch( + "salesforce_bulk.util.IteratorBytesIO", + return_value=StringIO('[{"Id": "001abc"}]'), + ): + result = retrieve_profile_api_instance._run_query(query) + + assert result == result_data + + +def test_extract_table_name_from_query_valid(retrieve_profile_api_instance): + query = "SELECT Id FROM Account" + table_name = retrieve_profile_api_instance._extract_table_name_from_query(query) + assert table_name == "Account" + + +def test_extract_table_name_from_query_invalid(retrieve_profile_api_instance): + query = "SELECT Id" + with pytest.raises(ValueError): + retrieve_profile_api_instance._extract_table_name_from_query(query) + + +def test_build_query_basic(retrieve_profile_api_instance): + columns = ["Id", "Name"] + table_name = "Account" + expected_query = "SELECT Id, Name FROM Account" + result_query = retrieve_profile_api_instance._build_query(columns, table_name) + assert result_query == expected_query + + +def test_build_query_with_where(retrieve_profile_api_instance): + columns = ["Id", "Name"] + table_name = "Account" + where = {"Type": "Customer", "Status": ["Active", "Inactive"]} + expected_query = "SELECT Id, Name FROM Account WHERE Type = 'Customer' AND Status IN ('Active', 'Inactive')" + result_query = retrieve_profile_api_instance._build_query( + columns, table_name, where + ) + assert result_query == expected_query + + +def test_build_query_empty_columns(retrieve_profile_api_instance): + columns = [] + table_name = "Account" + with pytest.raises(ValueError): + retrieve_profile_api_instance._build_query(columns, table_name) + + +def test_build_query_empty_table_name(retrieve_profile_api_instance): + columns = ["Id", "Name"] + table_name = "" + with pytest.raises(ValueError): + retrieve_profile_api_instance._build_query(columns, table_name) + + +def test_process_sObject_results(retrieve_profile_api_instance): + result_list = [ + {"SobjectType": "Account"}, + {"SobjectType": "Contact"}, + {"SobjectType": "Opportunity"}, + ] + expected_result = {"CustomObject": ["Account", "Contact", "Opportunity"]} + result = retrieve_profile_api_instance._process_sObject_results(result_list) + assert result == expected_result + + +def test_process_sObject_results_missing_key(retrieve_profile_api_instance): + result_list = [{"ObjectType": "Account"}] + with pytest.raises(KeyError): + retrieve_profile_api_instance._process_sObject_results(result_list) + + +def test_process_customTab_results(retrieve_profile_api_instance): + result_list = [ + {"Name": "CustomTab1"}, + {"Name": "CustomTab2"}, + {"Name": "CustomTab3"}, + ] + expected_result = {"CustomTab": ["CustomTab1", "CustomTab2", "CustomTab3"]} + result = retrieve_profile_api_instance._process_customTab_results(result_list) + assert result == expected_result + + +def test_process_customTab_results_missing_key(retrieve_profile_api_instance): + result_list = [{"TabName": "CustomTab1"}] + with pytest.raises(KeyError): + retrieve_profile_api_instance._process_customTab_results(result_list) + + +def test_process_setupEntityAccess_results(retrieve_profile_api_instance): + result_list = [ + {"SetupEntityType": "ApexClass", "SetupEntityId": "001abc"}, + {"SetupEntityType": "ApexPage", "SetupEntityId": "002def"}, + {"SetupEntityType": "CustomPermission", "SetupEntityId": "003ghi"}, + ] + + queries_result = { + "ApexClass": [ + {"Id": "001abc", "Name": "TestApexClass", "NamespacePrefix": "apex"} + ], + "ApexPage": [{"Id": "002def", "Name": "TestApexPage"}], + "CustomPermission": [], + } + with patch.object( + RetrieveProfileApi, "_build_query", return_value="SELECT Id, Name FROM Table" + ) as mock_build_query, patch.object( + RunParallelQueries, + "_run_queries_in_parallel", + return_value=queries_result, + ) as mock_run_queries: + + ( + entities, + result, + ) = retrieve_profile_api_instance._process_setupEntityAccess_results( + result_list + ) + mock_build_query.assert_any_call( + ["Name", "NamespacePrefix"], "ApexClass", {"Id": ["001abc"]} + ) + mock_build_query.assert_any_call( + ["Name", "NamespacePrefix"], "ApexPage", {"Id": ["002def"]} + ) + mock_build_query.assert_any_call( + ["DeveloperName", "NamespacePrefix"], "CustomPermission", {"Id": ["003ghi"]} + ) + mock_run_queries.assert_called_with( + { + "ApexClass": "SELECT Id, Name FROM Table", + "ApexPage": "SELECT Id, Name FROM Table", + "CustomPermission": "SELECT Id, Name FROM Table", + }, + retrieve_profile_api_instance._run_query, + ) + + expected_entities = { + "ApexClass": ["apex__TestApexClass"], + "ApexPage": ["TestApexPage"], + "CustomPermission": [], + } + assert entities == expected_entities + assert result == queries_result + + +def test_process_all_results(retrieve_profile_api_instance): + result_dict = { + "setupEntityAccess": "some_result", + "sObject": "some_result", + "customTab": "some_result", + "profileFlow": "some_result", + } + with patch.object( + RetrieveProfileApi, + "_process_setupEntityAccess_results", + return_value=( + { + "ApexClass": ["TestApexClass"], + "ApexPage": ["TestApexPage"], + "FlowDefinition": ["TestFlow"], + }, + {"FlowDefinition": ["some_result"]}, + ), + ), patch.object( + RetrieveProfileApi, + "_process_sObject_results", + return_value={"CustomObject": ["TestObject"]}, + ), patch.object( + RetrieveProfileApi, + "_process_customTab_results", + return_value={"CustomTab": ["TestTab"]}, + ), patch.object( + RetrieveProfileApi, + "_match_profiles_and_flows", + return_value={"Profile1": ["Flow1"]}, + ): + entities, profile_flow = retrieve_profile_api_instance._process_all_results( + result_dict + ) + + print(entities) + assert entities["ApexClass"] == ["TestApexClass"] + assert entities["ApexPage"] == ["TestApexPage"] + assert entities["FlowDefinition"] == ["TestFlow"] + assert entities["CustomObject"] == ["TestObject"] + assert entities["CustomTab"] == ["TestTab"] + assert profile_flow == {"Profile1": ["Flow1"]} + + +def test_queries_retrieve_permissions(retrieve_profile_api_instance): + profiles = ["Profile1", "Profile2"] + + with patch.object(RetrieveProfileApi, "_build_query") as mock_build_query: + retrieve_profile_api_instance._queries_retrieve_permissions(profiles) + + mock_build_query.assert_any_call( + ["SetupEntityId", "SetupEntityType"], + "SetupEntityAccess", + {"Parent.Profile.Name": profiles}, + ) + mock_build_query.assert_any_call( + ["SObjectType"], "ObjectPermissions", {"Parent.Profile.Name": profiles} + ) + mock_build_query.assert_any_call( + ["Name"], "PermissionSetTabSetting", {"Parent.Profile.Name": profiles} + ) + + +def test_retrieve_permissionable_entities(retrieve_profile_api_instance): + profiles = ["Profile1", "Profile2"] + expected_queries = {"query_name": "query"} + expected_result = ( + { + "ApexClass": ["TestApexClass"], + "ApexPage": ["TestApexPage"], + "CustomObject": ["TestObject"], + "CustomTab": ["TestTab"], + }, + {"Profile1": ["Flow1"]}, + ) + + with patch.object( + RunParallelQueries, "_run_queries_in_parallel" + ) as mock_run_queries, patch.object( + RetrieveProfileApi, + "_queries_retrieve_permissions", + return_value=expected_queries, + ), patch.object( + RetrieveProfileApi, "_process_all_results", return_value=expected_result + ): + + result = retrieve_profile_api_instance._retrieve_permissionable_entities( + profiles + ) + mock_run_queries.assert_called_with( + expected_queries, retrieve_profile_api_instance._run_query + ) + + assert result == expected_result + + +def test_match_profiles_and_flows(retrieve_profile_api_instance): + result_profiles = [ + {"SetupEntityId": "001abc", "Parent": {"Profile": {"Name": "Profile1"}}}, + {"SetupEntityId": "001abc", "Parent": {"Profile": {"Name": "Profile2"}}}, + {"SetupEntityId": "002def", "Parent": {"Profile": {"Name": "Profile1"}}}, + ] + + result_flows = [ + {"ApiName": "Flow1", "attributes": {"url": "instance_url/001abc"}}, + {"ApiName": "Flow2", "attributes": {"url": "instance_url/002def"}}, + ] + + profile_flow = retrieve_profile_api_instance._match_profiles_and_flows( + result_profiles, result_flows + ) + + assert "Flow1" in profile_flow["Profile1"] + assert "Flow2" in profile_flow["Profile1"] + assert "Flow1" in profile_flow["Profile2"] + assert "Flow2" not in profile_flow["Profile2"] + + +if __name__ == "__main__": + pytest.main() diff --git a/cumulusci/tasks/salesforce/retrieve_profile.py b/cumulusci/tasks/salesforce/retrieve_profile.py new file mode 100644 index 0000000000..b9bb3ece8c --- /dev/null +++ b/cumulusci/tasks/salesforce/retrieve_profile.py @@ -0,0 +1,172 @@ +import os + +from cumulusci.core.utils import process_bool_arg, process_list_arg +from cumulusci.salesforce_api.metadata import ApiRetrieveUnpackaged +from cumulusci.salesforce_api.retrieve_profile_api import RetrieveProfileApi +from cumulusci.tasks.salesforce.BaseSalesforceMetadataApiTask import ( + BaseSalesforceMetadataApiTask, +) + + +class RetrieveProfile(BaseSalesforceMetadataApiTask): + api_class = ApiRetrieveUnpackaged + task_options = { + "profiles": { + "description": "List of profile API names that you want to retrieve", + "required": True, + }, + "path": { + "description": "Target folder path. By default, it uses force-app/main/default", + }, + "strict_mode": { + "description": "When set to False, enables leniency by ignoring missing profiles when provided with a list of profiles." + " When set to True, enforces strict validation, causing a failure if any profile is not present in the list." + " Default is True", + "required": False, + }, + } + + def _init_options(self, kwargs): + super(RetrieveProfile, self)._init_options(kwargs) + self.api_version = self.org_config.latest_api_version + self.profiles = process_list_arg(self.options["profiles"]) + if not self.profiles: + raise ValueError("At least one profile must be specified.") + + self.extract_dir = self.options.get("path", "force-app/default/main") + + if not os.path.exists(self.extract_dir): + raise FileNotFoundError( + f"The extract directory '{self.extract_dir}' does not exist." + ) + + if not os.path.isdir(self.extract_dir): + raise NotADirectoryError(f"'{self.extract_dir}' is not a directory.") + + self.strictMode = process_bool_arg(self.options.get("strict_mode", True)) + + def _check_existing_profiles(self, retrieve_profile_api_task): + # Check for existing profiles + self.existing_profiles = retrieve_profile_api_task._retrieve_existing_profiles( + self.profiles + ) + self.missing_profiles = set(self.profiles) - set(self.existing_profiles) + + # Handle for strictMode + if self.missing_profiles: + self.logger.warning( + f"The following profiles were not found or could not be retrieved: '{', '.join(self.missing_profiles)}'\n" + ) + if self.strictMode: + raise RuntimeError( + "Operation failed due to missing profiles. Set strictMode to False if you want to ignore missing profiles." + ) + + # Handle for no existing profiles + if not self.existing_profiles: + raise RuntimeError("None of the profiles given were found.") + + def add_flow_accesses(self, profile_content, flows): + # Find the position of the closing tag + profile_end_position = profile_content.find("") + + if profile_end_position != -1: + flow_accesses_xml = "".join( + [ + f" \n" + f" true\n" + f" {flow}\n" + f" \n" + for flow in flows + ] + ) + modified_content = ( + profile_content[:profile_end_position] + + flow_accesses_xml + + profile_content[profile_end_position:] + ) + return modified_content + + return profile_content + + def save_profile_file(self, extract_dir, filename, content): + profile_path = os.path.join(extract_dir, filename) + os.makedirs(os.path.dirname(profile_path), exist_ok=True) + with open(profile_path, "w", encoding="utf-8") as updated_profile_file: + updated_profile_file.write(content) + + def _run_task(self): + self.retrieve_profile_api_task = RetrieveProfileApi( + project_config=self.project_config, + task_config=self.task_config, + org_config=self.org_config, + ) + self.retrieve_profile_api_task._init_task() + self._check_existing_profiles(self.retrieve_profile_api_task) + ( + permissionable_entities, + profile_flows, + ) = self.retrieve_profile_api_task._retrieve_permissionable_entities( + self.existing_profiles + ) + entities_to_be_retrieved = { + **permissionable_entities, + **{"Profile": self.existing_profiles}, + } + + self.package_xml = self._create_package_xml( + entities_to_be_retrieved, self.api_version + ) + api = self._get_api() + zip_result = api() + + for file_info in zip_result.infolist(): + if file_info.filename.startswith( + "profiles/" + ) and file_info.filename.endswith(".profile"): + with zip_result.open(file_info) as profile_file: + profile_content = profile_file.read().decode("utf-8") + profile_name = os.path.splitext( + os.path.basename(file_info.filename) + )[0] + + if profile_name in profile_flows: + profile_content = self.add_flow_accesses( + profile_content, profile_flows[profile_name] + ) + + self.save_profile_file( + self.extract_dir, file_info.filename, profile_content + ) + + # zip_result.extractall('./unpackaged') + + self.logger.info( + f"Profiles {', '.join(self.existing_profiles)} unzipped into folder '{self.extract_dir}'" + ) + + def _get_api(self): + # Logs + self.logger.info("Retrieving all entities from org:") + + return self.api_class( + self, + api_version=self.api_version, + package_xml=self.package_xml, + ) + + def _create_package_xml(self, input_dict: dict, api_version: str): + package_xml = '\n' + package_xml += '\n' + + for name, members in input_dict.items(): + package_xml += " \n" + for member in members: + package_xml += f" {member}\n" + package_xml += f" {name}\n" + package_xml += " \n" + + package_xml += f" {api_version}\n" + package_xml += "\n" + + return package_xml diff --git a/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py b/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py new file mode 100644 index 0000000000..7f34e39581 --- /dev/null +++ b/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py @@ -0,0 +1,253 @@ +import logging +import os +import xml.etree.ElementTree as ET +import zipfile +from tempfile import NamedTemporaryFile +from unittest.mock import MagicMock, patch + +import pytest + +from cumulusci.core.config import TaskConfig +from cumulusci.salesforce_api.metadata import ApiRetrieveUnpackaged +from cumulusci.salesforce_api.retrieve_profile_api import RetrieveProfileApi +from cumulusci.tasks.salesforce.retrieve_profile import RetrieveProfile + + +@pytest.fixture +def retrieve_profile_task(tmpdir): + project_config = MagicMock() + task_config = TaskConfig( + { + "options": { + "profiles": ["Profile1", "Profile2"], + "path": tmpdir, + "strict_mode": False, + } + } + ) + org_config = MagicMock() + task = RetrieveProfile(project_config, task_config, org_config) + task.logger = logging.getLogger(__name__) + task.logger.setLevel(logging.DEBUG) + return task + + +def test_check_existing_profiles_with_missing_profiles_and_strict_mode_enabled(tmpdir): + project_config = MagicMock() + task_config = TaskConfig( + { + "options": { + "profiles": ["Profile1", "Profile2"], + "path": tmpdir, + "strict_mode": True, + } + } + ) + org_config = MagicMock() + retrieve_profile_task = RetrieveProfile(project_config, task_config, org_config) + with patch.object( + RetrieveProfileApi, "_retrieve_existing_profiles", return_value=["Profile1"] + ): + with pytest.raises( + RuntimeError, match="Operation failed due to missing profiles" + ): + retrieve_profile_task._check_existing_profiles(RetrieveProfileApi) + + +def test_check_existing_profiles_with_no_existing_profiles(tmpdir): + project_config = MagicMock() + task_config = TaskConfig( + { + "options": { + "profiles": ["Profile1", "Profile2"], + "path": tmpdir, + "strict_mode": False, + } + } + ) + org_config = MagicMock() + retrieve_profile_task = RetrieveProfile(project_config, task_config, org_config) + with patch.object( + RetrieveProfileApi, "_retrieve_existing_profiles", return_value=[] + ): + with pytest.raises( + RuntimeError, match="None of the profiles given were found." + ): + retrieve_profile_task._check_existing_profiles(RetrieveProfileApi) + + +def test_init_options(retrieve_profile_task): + retrieve_profile_task._init_options(retrieve_profile_task.task_config.config) + assert retrieve_profile_task.profiles == ["Profile1", "Profile2"] + assert retrieve_profile_task.strictMode is False + + +def test_init_options_raises_error_with_no_profiles(): + project_config = MagicMock() + task_config = TaskConfig({"options": {"profiles": None}}) + org_config = MagicMock() + + with pytest.raises(ValueError) as exc_info: + RetrieveProfile(project_config, task_config, org_config) + + assert str(exc_info.value) == "At least one profile must be specified." + + +def test_init_options_raises_error_with_invalid_path_directory(): + project_config = MagicMock() + task_config = TaskConfig( + {"options": {"profiles": ["Profile1"], "path": "/nonexistent/directory"}} + ) + org_config = MagicMock() + + with pytest.raises(FileNotFoundError) as exc_info: + RetrieveProfile(project_config, task_config, org_config) + + expected_message = "The extract directory '/nonexistent/directory' does not exist." + assert str(exc_info.value) == expected_message + + +def test_init_options_raises_error_with_non_directory_path(tmp_path): + tmpfile = tmp_path / "file.txt" + tmpfile.write_text("Something") + project_config = MagicMock() + task_config = TaskConfig({"options": {"profiles": ["Profile1"], "path": tmpfile}}) + org_config = MagicMock() + + with pytest.raises(NotADirectoryError) as exc_info: + RetrieveProfile(project_config, task_config, org_config) + + expected_message = f"'{tmpfile}' is not a directory." + assert str(exc_info.value) == expected_message + + +def create_temp_zip_file(): + temp_zipfile = NamedTemporaryFile(delete=True) + + with zipfile.ZipFile(temp_zipfile, "w", zipfile.ZIP_DEFLATED) as zipf: + zipf.writestr("profiles/Profile1.profile", "") + + return zipfile.ZipFile(temp_zipfile, "r") + + +def test_save_profile_file(retrieve_profile_task, tmpdir): + extract_dir = str(tmpdir) + filename = "TestProfile.profile" + content = "Profile content" + expected_file_path = os.path.join(extract_dir, filename) + retrieve_profile_task.save_profile_file(extract_dir, filename, content) + + assert os.path.exists(expected_file_path) + with open(expected_file_path, "r", encoding="utf-8") as profile_file: + saved_content = profile_file.read() + assert saved_content == content + + +def test_add_flow_accesses(retrieve_profile_task): + profile_content = "\n" " Hello\n" "" + flows = ["Flow1", "Flow2"] + expected_content = ( + "\n" + " Hello\n" + " \n" + " true\n" + " Flow1\n" + " \n" + " \n" + " true\n" + " Flow2\n" + " \n" + "" + ) + modified_content = retrieve_profile_task.add_flow_accesses(profile_content, flows) + assert modified_content == expected_content + + # Content without the tag + profile_content = "\n" " Hello\n" + modified_content = retrieve_profile_task.add_flow_accesses(profile_content, flows) + assert modified_content == profile_content + + +def test_run_task(retrieve_profile_task, tmpdir, caplog): + retrieve_profile_task.extract_dir = tmpdir + temp_zipfile = create_temp_zip_file() + + with patch.object( + RetrieveProfileApi, + "_retrieve_permissionable_entities", + return_value=({"ApexClass": ["TestApexClass"]}, {"Profile1": ["Flow1"]}), + ), patch.object( + RetrieveProfileApi, "_init_task", return_value="something" + ), patch.object( + ApiRetrieveUnpackaged, "__call__", return_value=temp_zipfile + ), patch.object( + RetrieveProfileApi, "_retrieve_existing_profiles", return_value=["Profile1"] + ): + retrieve_profile_task._run_task() + + assert os.path.exists(tmpdir) + profile1_path = os.path.join(tmpdir, "profiles/Profile1.profile") + assert os.path.exists(profile1_path) + + log_messages = [record.message for record in caplog.records] + assert f"Profiles Profile1 unzipped into folder '{tmpdir}'" in log_messages + assert ( + f"Profiles Profile1, Profile2 unzipped into folder '{tmpdir}'" + not in log_messages + ) + assert ( + "The following profiles were not found or could not be retrieved: 'Profile2'\n" + in log_messages + ) + + +def test_get_api(retrieve_profile_task): + retrieve_profile_task.package_xml = "" + api = retrieve_profile_task._get_api() + assert isinstance(api, ApiRetrieveUnpackaged) + + +def remove_whitespace(xml_str): + return "".join(line.strip() for line in xml_str.splitlines()) + + +def compare_xml_strings(xml_str1, xml_str2): + xml_str1 = remove_whitespace(xml_str1) + xml_str2 = remove_whitespace(xml_str2) + + tree1 = ET.ElementTree(ET.fromstring(xml_str1)) + tree2 = ET.ElementTree(ET.fromstring(xml_str2)) + + parsed_xml_str1 = ET.tostring(tree1.getroot(), encoding="unicode") + parsed_xml_str2 = ET.tostring(tree2.getroot(), encoding="unicode") + + return parsed_xml_str1 == parsed_xml_str2 + + +def test_create_package_xml(retrieve_profile_task): + input_dict = { + "ApexClass": ["Class1", "Class2"], + "Profile": ["Profile1", "Profile2"], + } + package_xml = retrieve_profile_task._create_package_xml(input_dict, "58.0") + + expected_package_xml = """ + + + Class1 + Class2 + ApexClass + + + Profile1 + Profile2 + Profile + + 58.0 + """ + + assert compare_xml_strings(package_xml, expected_package_xml) + + +if __name__ == "__main__": + pytest.main() diff --git a/cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py b/cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py new file mode 100644 index 0000000000..2db64811bb --- /dev/null +++ b/cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py @@ -0,0 +1,29 @@ +from concurrent.futures import ThreadPoolExecutor +from typing import Callable, Dict + + +class RunParallelQueries: + @staticmethod + def _run_queries_in_parallel( + queries: Dict[str, str], run_query: Callable[[str], dict], num_threads: int = 4 + ) -> Dict[str, list]: + """Accepts a set of queries structured as {'query_name': 'query'} + and a run_query function that runs a particular query. Runs queries in parallel and returns the queries""" + results_dict = {} + + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = { + query_name: executor.submit(run_query, query) + for query_name, query in queries.items() + } + + for query_name, future in futures.items(): + try: + query_result = future.result() + results_dict[query_name] = query_result["records"] + except Exception as e: + raise Exception(f"Error executing query '{query_name}': {type(e)}: {e}") + else: + queries.pop(query_name, None) + + return results_dict diff --git a/cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py b/cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py new file mode 100644 index 0000000000..725cf8364f --- /dev/null +++ b/cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py @@ -0,0 +1,50 @@ +from concurrent.futures import Future, ThreadPoolExecutor +from unittest.mock import MagicMock, patch + +import pytest + +from cumulusci.utils.parallel.queries_in_parallel.run_queries_in_parallel import ( + RunParallelQueries, +) + + +def run_query(query): + return {"test": query} + + +def test_run_queries_in_parallel(): + queries = { + "query1": "SELECT * FROM Table1", + "query2": "SELECT * FROM Table2", + } + + expected_results = { + "query1": [{"field1": "value1"}, {"field1": "value2"}], + "query2": [{"field2": "value3"}, {"field2": "value4"}], + } + + mock_future1 = MagicMock() + mock_future1.result.return_value = {"records": expected_results["query1"]} + mock_future2 = MagicMock() + mock_future2.result.return_value = {"records": expected_results["query2"]} + + with patch.object(ThreadPoolExecutor, "submit") as mock_submit: + mock_submit.side_effect = [mock_future1, mock_future2] + + results_dict = RunParallelQueries._run_queries_in_parallel(queries, run_query) + + assert results_dict == expected_results + + +def test_run_queries_in_parallel_with_exception(): + queries = { + "Query1": "SELECT Id FROM Table1", + } + + with patch.object(Future, "result", side_effect=Exception("Test exception")): + with pytest.raises(Exception) as excinfo: + RunParallelQueries._run_queries_in_parallel(queries, run_query=run_query) + assert ( + "Error executing query 'Query1': : Test exception" + == str(excinfo.value) + ) From e3a656c1674437bcad9d1245e804acbeb66f148c Mon Sep 17 00:00:00 2001 From: James Estevez Date: Tue, 7 Nov 2023 09:03:18 -0800 Subject: [PATCH 72/98] Remove `robot_lint` task and dependencies (#3697) This PR removes the built-in Robot Framework linting task in favor of external tools. Instead, we're pointing users to an external tool, `robotframework-robocop`. Changes: - Removed `robot_lint` task and tests. - Cleaned up related dependencies. - Updated `robot.md` to guide users to `robotframework-robocop`. Release note: - **Critical Change**: Removed the `robot_lint` task from CumulusCI. Users relying on this linting functionality should transition to using [`robotframework-robocop`](https://robocop.readthedocs.io/). --- cumulusci/cumulusci.yml | 4 - cumulusci/tasks/robotframework/__init__.py | 1 - cumulusci/tasks/robotframework/lint.py | 212 --------------- .../tasks/robotframework/lint_defaults.txt | 25 -- .../robotframework/tests/test_robot_lint.py | 242 ------------------ ...2-remove-robot-lint-task-from-cumulusci.md | 79 ++++++ docs/robot.md | 7 +- pyproject.toml | 1 - requirements/dev.txt | 3 - requirements/prod.txt | 3 - 10 files changed, 83 insertions(+), 494 deletions(-) delete mode 100644 cumulusci/tasks/robotframework/lint.py delete mode 100644 cumulusci/tasks/robotframework/lint_defaults.txt delete mode 100644 cumulusci/tasks/robotframework/tests/test_robot_lint.py create mode 100644 docs/adrs/0002-remove-robot-lint-task-from-cumulusci.md diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 301fe6adf6..87f75f38b6 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -552,10 +552,6 @@ tasks: output: Keywords.html title: $project_config.project__package__name group: Robot Framework - robot_lint: - description: Static analysis tool for robot framework files - class_path: cumulusci.tasks.robotframework.RobotLint - group: Robot Framework robot_testdoc: description: Generates html documentation of your Robot test suite and writes to tests/test_suite. class_path: cumulusci.tasks.robotframework.RobotTestDoc diff --git a/cumulusci/tasks/robotframework/__init__.py b/cumulusci/tasks/robotframework/__init__.py index 107296b810..bc1d314d9b 100644 --- a/cumulusci/tasks/robotframework/__init__.py +++ b/cumulusci/tasks/robotframework/__init__.py @@ -1,4 +1,3 @@ from cumulusci.tasks.robotframework.robotframework import Robot # noqa: F401 from cumulusci.tasks.robotframework.robotframework import RobotTestDoc # noqa: F401 from cumulusci.tasks.robotframework.libdoc import RobotLibDoc # noqa: F401 -from cumulusci.tasks.robotframework.lint import RobotLint # noqa: F401 diff --git a/cumulusci/tasks/robotframework/lint.py b/cumulusci/tasks/robotframework/lint.py deleted file mode 100644 index 40726e26ea..0000000000 --- a/cumulusci/tasks/robotframework/lint.py +++ /dev/null @@ -1,212 +0,0 @@ -import glob -import os -from pathlib import Path - -import rflint - -from cumulusci.core.exceptions import CumulusCIFailure -from cumulusci.core.tasks import BaseTask -from cumulusci.core.utils import process_bool_arg, process_list_arg - - -class RobotLint(BaseTask): - task_docs = """ - The robot_lint task performs static analysis on one or more .robot - and .resource files. Each line is parsed, and the result passed through - a series of rules. Rules can issue warnings or errors about each line. - - If any errors are reported, the task will exit with a non-zero status. - - When a rule has been violated, a line will appear on the output in - the following format: - - **: **, **: ** (**) - - - ** will be either W for warning or E for error - - ** is the line number where the rule was triggered - - ** is the character where the rule was triggered, - or 0 if the rule applies to the whole line - - ** is a short description of the issue - - ** is the name of the rule that raised the issue - - Note: the rule name can be used with the ignore, warning, error, - and configure options. - - Some rules are configurable, and can be configured with the - `configure` option. This option takes a list of values in the form - **:**,**:**,etc. For example, to set - the line length for the LineTooLong rule you can use '-o configure - LineTooLong:80'. If a rule is configurable, it will show the - configuration options in the documentation for that rule - - The filename will be printed once before any errors or warnings - for that file. The filename is preceeded by `+` - - Example Output:: - - + example.robot - W: 2, 0: No suite documentation (RequireSuiteDocumentation) - E: 30, 0: No testcase documentation (RequireTestDocumentation) - - To see a list of all configured rules, set the 'list' option to True: - - cci task run robot_lint -o list True - - """ - - task_options = { # TODO: should use `class Options instead` - "configure": { - "description": "List of rule configuration values, in the form of rule:args.", - "default": None, - }, - "ignore": { - "description": "List of rules to ignore. Use 'all' to ignore all rules", - "default": None, - }, - "error": { - "description": "List of rules to treat as errors. Use 'all' to affect all rules.", - "default": None, - }, - "warning": { - "description": "List of rules to treat as warnings. Use 'all' to affect all rules.", - "default": None, - }, - "list": { - "description": "If option is True, print a list of known rules instead of processing files.", - "default": None, - }, - "path": { - "description": "The path to one or more files or folders. " - "If the path includes wildcard characters, they will be expanded. " - "If not provided, the default will be to process all files " - "under robot/", - "required": False, - }, - } - - def _validate_options(self): - super(RobotLint, self)._validate_options() - self.options["list"] = process_bool_arg(self.options.get("list", False)) - - self.options["path"] = process_list_arg(self.options.get("path", None)) - self.options["ignore"] = process_list_arg(self.options.get("ignore", [])) - self.options["warning"] = process_list_arg(self.options.get("warning", [])) - self.options["error"] = process_list_arg(self.options.get("error", [])) - self.options["configure"] = process_list_arg(self.options.get("configure", [])) - - if self.options["path"] is None: - self.options["path"] = [ - "robot/{}".format(self.project_config.project["name"]) - ] - - def _run_task(self): - linter = CustomRfLint(task=self) - result = 0 - - if self.options["list"]: - args = self._get_args() - linter.run(args + ["--list"]) - - else: - files = self._get_files() - args = self._get_args() - - # the result is a count of the number of errors, - # though I don't think the caller cares. - result = linter.run(args + sorted(files)) - - # result is the number of errors detected - if result > 0: - message = ( - "1 error was detected" - if result == 1 - else "{} errors were detected".format(result) - ) - raise CumulusCIFailure(message) - - def _get_files(self): - """Returns the working set of files to be processed""" - expanded_paths = set() - for path in self.options["path"]: - expanded_paths.update(glob.glob(path)) - - all_files = set() - for path in expanded_paths: - if os.path.isdir(path): - for root, dirs, files in os.walk(path): - for filename in files: - if filename.endswith(".robot") or filename.endswith( - ".resource" - ): - all_files.add(os.path.join(root, filename)) - else: - all_files.add(path) - return all_files - - def _get_args(self): - """Return rflint-style args based on the task options""" - - here = Path(__file__).parent - args = ["--argumentfile", str((here / "lint_defaults.txt").resolve())] - - for rule in self.options["ignore"]: - args.extend(["--ignore", rule]) - for rule in self.options["warning"]: - args.extend(["--warning", rule]) - for rule in self.options["error"]: - args.extend(["--error", rule]) - for config in self.options["configure"]: - args.extend(["--configure", config]) - return args - - -# A better solution might be to modify rflint so we can pass in a -# function it can use to write the message. We'll save that for a -# later day. This works fine, though I grudgingly had to steal a -# little of its internal logic to keep track of the filename. -class CustomRfLint(rflint.RfLint): - """Wrapper around RfLint to support using the task logger""" - - def __init__(self, task): - rflint.RfLint.__init__(self) - self.task = task - - def list_rules(self): - """Print a list of all rules""" - # note: rflint supports terse and verbose output. I don't - # think the terse output is very useful, so this will always - # print all of the documentation. - for rule in sorted(self.all_rules, key=lambda rule: rule.name): - self.task.logger.info("") - self.task.logger.info(rule) - for line in rule.doc.split("\n"): - self.task.logger.info(" " + line) - - def report(self, linenumber, filename, severity, message, rulename, char): - if self._print_filename is not None: - # we print the filename only once. self._print_filename - # will get reset each time a new file is processed. - self.task.logger.info("+ " + self._print_filename) - self._print_filename = None - - if severity in (rflint.WARNING, rflint.ERROR): - self.counts[severity] += 1 - logger = ( - self.task.logger.warn - if severity == rflint.WARNING - else self.task.logger.error - ) - else: - self.counts["other"] += 1 - logger = self.task.logger.error - - logger( - self.args.format.format( - linenumber=linenumber, - filename=filename, - severity=severity, - message=message, - rulename=rulename, - char=char, - ) - ) diff --git a/cumulusci/tasks/robotframework/lint_defaults.txt b/cumulusci/tasks/robotframework/lint_defaults.txt deleted file mode 100644 index 52d2898389..0000000000 --- a/cumulusci/tasks/robotframework/lint_defaults.txt +++ /dev/null @@ -1,25 +0,0 @@ -# Default arguments for the rflint task -# These can be overridden from command line or in -# cumulusci.yml ---ignore TooManyTestSteps ---warning DuplicateVariablesInResource ---warning DuplicateVariablesInSuite ---warning FileTooLong ---warning LineTooLong ---warning RequireSuiteDocumentation ---warning TooManyTestCases ---error DuplicateKeywordNames ---error DuplicateSettingsInResource ---error DuplicateSettingsInSuite ---error DuplicateTestNames ---error InvalidTable ---error InvalidTableInResource ---error PeriodInSuiteName ---error PeriodInTestName ---error RequireKeywordDocumentation ---error RequireTestDocumentation ---error TagWithSpaces ---error TooFewKeywordSteps ---error TooFewTestSteps ---error TrailingBlankLines ---error TrailingWhitespace diff --git a/cumulusci/tasks/robotframework/tests/test_robot_lint.py b/cumulusci/tasks/robotframework/tests/test_robot_lint.py deleted file mode 100644 index 1e73076af5..0000000000 --- a/cumulusci/tasks/robotframework/tests/test_robot_lint.py +++ /dev/null @@ -1,242 +0,0 @@ -import os.path -import shutil -import tempfile -import textwrap -from pathlib import Path - -import pytest - -from cumulusci.core.config import TaskConfig -from cumulusci.core.exceptions import CumulusCIFailure -from cumulusci.core.tests.utils import MockLoggerMixin -from cumulusci.tasks.robotframework import RobotLint -from cumulusci.tasks.salesforce.tests.util import create_task -from cumulusci.tests.util import create_project_config - - -class TestRobotLint(MockLoggerMixin): - def setup_method(self): - self.tmpdir = tempfile.mkdtemp(dir=".") - self.task_config = TaskConfig() - self._task_log_handler.reset() - self.task_log = self._task_log_handler.messages - - # define base_args, which are arguments that the task adds - # before any user-supplied arguments - here = Path(__file__).parent.parent - lint_defaults = str((here / "lint_defaults.txt").resolve()) - self.base_args = ["--argumentfile", lint_defaults] - - def teardown_method(self): - shutil.rmtree(self.tmpdir) - - def make_test_file(self, data, suffix=".robot", name="test", dir=None): - """Create a temporary test file""" - dir = self.tmpdir if dir is None else dir - filename = os.path.join(dir, "{}{}".format(name, suffix)) - with open(filename, "w") as f: - f.write(textwrap.dedent(data)) - return filename - - def test_no_duplicate_files(self): - """Verify that the working set of files has no duplicates""" - path = self.make_test_file( - """ - *** Test Cases *** - Example - log hello, world - """, - suffix=".robot", - ) - glob_path = "{}/*.robot".format(self.tmpdir) - - task = create_task( - RobotLint, - { - "path": [glob_path, path], - "ignore": "all", - "error": "RequireTestDocumentation", - }, - ) - expected = "1 error was detected" - with pytest.raises(CumulusCIFailure, match=expected): - task() - - def test_exception_on_error(self): - """Verify CumulusCIFailure is thrown on rule violations""" - path = self.make_test_file( - """ - *** Test Cases *** - Example - log hello, world - """ - ) - task = create_task( - RobotLint, - {"path": path, "ignore": "all", "error": "RequireTestDocumentation"}, - ) - expected = "1 error was detected" - with pytest.raises(CumulusCIFailure, match=expected): - task() - assert len(self.task_log["error"]) == 1 - assert self.task_log["error"] == [ - "E: 4, 0: No testcase documentation (RequireTestDocumentation)" - ] - - def test_unicode_filenames(self): - """Verify this task works with files that have unicode characters in the filename""" - path = self.make_test_file( - """ - *** Keywords *** - Example - # no documentation or body - """, - name="\u2601", - ) - task = create_task( - RobotLint, - {"path": path, "ignore": "all", "error": "RequireKeywordDocumentation"}, - ) - assert len(self.task_log["error"]) == 0 - expected = "1 error was detected" - - with pytest.raises(CumulusCIFailure, match=expected): - task() - - def test_rule_defaults(self): - """Verify we pass the default rules to rflint""" - - task = create_task(RobotLint, {"path": self.tmpdir}) - assert task._get_args() == self.base_args - - def test_configure_option(self): - """Verify that rule configuration options are passed to rflint""" - task = create_task( - RobotLint, - {"path": self.tmpdir, "configure": "LineTooLong:40,FileTooLong:123"}, - ) - expected = self.base_args + [ - "--configure", - "LineTooLong:40", - "--configure", - "FileTooLong:123", - ] - assert task._get_args() == expected - - def test_error_option(self): - """Verify that error option is propertly translated to rflint options""" - task = create_task( - RobotLint, {"path": self.tmpdir, "error": "LineTooLong,FileTooLong"} - ) - expected = self.base_args + ["--error", "LineTooLong", "--error", "FileTooLong"] - assert task._get_args() == expected - - def test_ignore_option(self): - """Verify that ignore option is propertly translated to rflint options""" - task = create_task( - RobotLint, - {"path": self.tmpdir, "ignore": "TooFewKeywordSteps,TooFewTestSteps"}, - ) - expected = self.base_args + [ - "--ignore", - "TooFewKeywordSteps", - "--ignore", - "TooFewTestSteps", - ] - assert task._get_args() == expected - - def test_warning_option(self): - """Verify that warning option is propertly translated to rflint options""" - task = create_task( - RobotLint, - {"path": self.tmpdir, "warning": "TrailingBlankLines, TrailingWhitespace"}, - ) - expected = self.base_args + [ - "--warning", - "TrailingBlankLines", - "--warning", - "TrailingWhitespace", - ] - assert task._get_args() == expected - - def test_ignore_all(self): - """Verify that -o ignore all works as expected - - We already have a test that verifies that the ignore options - are properly translated to rflint arguments. This is more of a sanity - check that it actually has the desired effect when the task is run. - """ - path = self.make_test_file( - """ - *** Keywords *** - Duplicate keyword name - # no documentation or body - Duplicate keyword name - # no documentation or body - *** Test Cases *** - Duplicate testcase name - # no documentation or body - Duplicate testcase name - """ - ) - task = create_task(RobotLint, {"path": path, "ignore": "all"}) - task() - assert len(self.task_log["warning"]) == 0 - assert len(self.task_log["error"]) == 0 - assert len(self.task_log["critical"]) == 0 - assert len(self.task_log["debug"]) == 0 - - def test_default_path(self): - """Verify that if no path is provided, we search robot/""" - - project_config = create_project_config() - project_config.config["project"]["name"] = "TestPackage" - task = create_task(RobotLint, {}, project_config=project_config) - assert task.options["path"] == ["robot/TestPackage"] - - def test_explicit_path(self): - """Verify an explicit path is used when given - - This also verifies that the path is converted to a proper list - if it is a comma-separated string. - """ - task = create_task(RobotLint, {"path": "/tmp/tests,/tmp/resources"}) - assert task.options["path"] == ["/tmp/tests", "/tmp/resources"] - - def test_wildcards(self): - """Verify that wildcards in the path are expanded""" - file1 = self.make_test_file("", name="a", suffix=".resource") - file2 = self.make_test_file("", name="b", suffix=".robot") - file3 = self.make_test_file("", name="c", suffix=".robot") - - # path with one wildcard should find one file - task = create_task(RobotLint, {"path": "{}/*.resource".format(self.tmpdir)}) - files = sorted(task._get_files()) - assert files == [file1] - - # two paths with wildcards should find all three files - task = create_task( - RobotLint, - {"path": "{dir}/*.resource, {dir}/*.robot".format(dir=self.tmpdir)}, - ) - files = sorted(task._get_files()) - assert files == [file1, file2, file3] - - def test_folder_for_path(self): - """Verify that if the path is a folder, we process all files in the folder""" - file1 = self.make_test_file("", name="a", suffix=".resource") - file2 = self.make_test_file("", name="b", suffix=".robot") - file3 = self.make_test_file("", name="c", suffix=".robot") - - task = create_task(RobotLint, {"path": self.tmpdir}) - files = sorted(task._get_files()) - assert files == [file1, file2, file3] - - def test_recursive_folder(self): - """Verify that subdirectories are included when finding files""" - subdir = tempfile.mkdtemp(dir=self.tmpdir) - file1 = self.make_test_file("", dir=subdir) - - task = create_task(RobotLint, {"path": self.tmpdir}) - files = sorted(task._get_files()) - assert files == [file1] diff --git a/docs/adrs/0002-remove-robot-lint-task-from-cumulusci.md b/docs/adrs/0002-remove-robot-lint-task-from-cumulusci.md new file mode 100644 index 0000000000..00c056a45c --- /dev/null +++ b/docs/adrs/0002-remove-robot-lint-task-from-cumulusci.md @@ -0,0 +1,79 @@ +--- +date: 2023-11-03 +status: Accepted +author: "@jstvz" +--- + + + +# 2. Remove `robot_lint` task from CumulusCI + +## Context and Problem Statement + +We encountered a compatibility issue with the `robotframework-lint` library in CumulusCI when running on Python 3.12. This problem, coupled with the library's lack of recent activity and our need to support Python 3.12 soon, prompts a decision about our approach to linting within CumulusCI. + +### Assumptions + +- Users who require linting functionality can install tools directly. +- The majority of our users won't be critically affected by changes to the linting task. + +### Constraints + +- We aim to minimize maintenance overhead for the CumulusCI team. +- The solution should not introduce significant new dependencies or complications. + +## Decision + +### Considered Options + +1. **Forking `robotframework-lint`** + + - **Pros**: + - Direct control and maintenance of the library. + - Ability to quickly address future issues or feature requests. + - **Cons**: + - Introduces maintenance overhead. + - Uncertainty about the long-term activity or viability of the library. + +2. **Vendoring the library** + + - **Pros**: + - CumulusCI always ships with a working version. + - No reliance on external repository maintenance. + - **Cons**: + - Challenges in updating the library. + - Increases the project's size and complexity. + +3. **Transitioning to [`robotframework-robocop`](https://github.com/MarketSquare/robotframework-robocop)** + + - **Pros**: + - More active and potentially more feature-rich library. + - Opportunity to benefit from community advancements. + - **Cons**: + - Introduces new dependencies. + - Might not have exact parity with the current tool. + - Requires rewriting `robot_lint` task. + +4. **Complete removal of `robot_lint`** + - **Pros**: + - Simplifies CumulusCI's codebase. + - Reduces maintenance overhead. + - **Cons**: + - Breaking change for users relying on the built-in task. + - Users must handle their linting setups. + +### Decision Outcome + +**Completely remove `robot_lint`** from CumulusCI. Direct users who need linting functionality to install the necessary tool (`robotframework-lint` or `robotframework-robocop`) directly. This approach offers a cleaner and more maintainable path forward, even though it introduces a breaking change. + +## Consequences + +- Users relying on the `robot_lint` task will need to adjust their setups. They'll need to directly install their linting tool of choice. +- CumulusCI's codebase will be cleaner and easier to maintain without the additional linting task. +- We'll need to communicate the change effectively to minimize disruption for users. + +## References + +- [Slack discussion](https://salesforce-internal.slack.com/archives/G024TDY0P18/p1699034856029979) +- [Issue highlighted by Stewart Anderson](https://github.com/boakley/robotframework-lint/issues/95) +- [PR for the imp to importlib transition](https://github.com/boakley/robotframework-lint/pull/96) diff --git a/docs/robot.md b/docs/robot.md index 80418acc3b..cfe246cf1b 100644 --- a/docs/robot.md +++ b/docs/robot.md @@ -232,9 +232,6 @@ CumulusCI integrates with Robot via custom tasks, such as: [testdoc](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#test-data-documentation-tool-testdoc) command, which creates an HTML file documenting all the tests in a test suite. -- `robot_lint`: Runs the static analysis tool - [rflint](https://github.com/boakley/robotframework-lint/), which can - validate Robot tests against a set of rules related to code quality. Like with any CumulusCI task, you can get documentation and a list of arguments with the `cci task info` command. For example, @@ -308,6 +305,10 @@ test case file. For more details on Robot syntax, visit the official [Robot syntax documentation](http://robotframework.org/robotframework/2.9.2/RobotFrameworkUserGuide.html#test-data-syntax). +```{tip} +For users interested in linting their Robot Framework code, we recommend checking out +[`robotframework-robocop`](https://robocop.readthedocs.io/). It's a static code analysis tool for Robot Framework that can help maintain good code quality. +``` + ### Settings The `Settings` section of the `.robot` file sets up the entire test diff --git a/pyproject.toml b/pyproject.toml index 3b494008e3..5a53cb15e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ dependencies = [ "requests-futures", "rich", "robotframework", - "robotframework-lint", "robotframework-pabot", "robotframework-requests", "robotframework-seleniumlibrary<6", diff --git a/requirements/dev.txt b/requirements/dev.txt index 626507d8e5..c5f76d24ed 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -256,13 +256,10 @@ rich==13.6.0 robotframework==6.1.1 # via # cumulusci (pyproject.toml) - # robotframework-lint # robotframework-pabot # robotframework-requests # robotframework-seleniumlibrary # robotframework-stacktrace -robotframework-lint==1.1 - # via cumulusci (pyproject.toml) robotframework-pabot==2.16.0 # via cumulusci (pyproject.toml) robotframework-pythonlibcore==4.2.0 diff --git a/requirements/prod.txt b/requirements/prod.txt index ebb157e3d8..3939c539ea 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -120,13 +120,10 @@ rich==13.6.0 robotframework==6.1.1 # via # cumulusci (pyproject.toml) - # robotframework-lint # robotframework-pabot # robotframework-requests # robotframework-seleniumlibrary # robotframework-stacktrace -robotframework-lint==1.1 - # via cumulusci (pyproject.toml) robotframework-pabot==2.16.0 # via cumulusci (pyproject.toml) robotframework-pythonlibcore==4.2.0 From 1d96879a3689dbc994d54f9ccc6abb31f84c95dc Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 26 Oct 2023 11:28:14 -0700 Subject: [PATCH 73/98] Remove invalid assertions `called_once_with` is not a valid (assertion) method and therefore an instance of mock.Mock is returned. Because instances of mock.Mock evaluate to true, the assertion is equivalent to assert True. See: https://discuss.python.org/t/include-prefix-called-in-list-of-forbidden-method-prefixes-for-mock-objects-in-unsafe-mode/22249/4 --- cumulusci/cli/tests/test_plan.py | 2 +- .../tests/test_encrypted_file_project_keychain.py | 4 +++- cumulusci/tasks/bulkdata/tests/test_step.py | 2 +- cumulusci/tasks/release_notes/tests/test_task.py | 6 +++--- .../tasks/salesforce/tests/test_PackageUpload.py | 15 +++++++-------- cumulusci/tests/test_main.py | 2 +- cumulusci/utils/tests/test_logging.py | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cumulusci/cli/tests/test_plan.py b/cumulusci/cli/tests/test_plan.py index 8bd424a2f2..1dbc55da38 100644 --- a/cumulusci/cli/tests/test_plan.py +++ b/cumulusci/cli/tests/test_plan.py @@ -70,7 +70,7 @@ def test_plan_list(self, cli_table, runtime): run_click_command(plan.plan_list, runtime=runtime, print_json=False) - cli_table.called_once_with( + cli_table.assert_called_once_with( data=[ ["Name", "Title", "Slug", "Tier"], ["plan 1", "Test Plan #1", "plan1_slug", "primary"], diff --git a/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py b/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py index 9360f71629..c761ed6bef 100644 --- a/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +++ b/cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py @@ -1173,7 +1173,9 @@ def test_set_and_get_org_with_bad_datatypes(self, keychain, org_config, key): org_config.config["bad"] = 25j keychain.set_org(org_config, True) - assert dumps.called_once_with({"bad", 25j}) + dumps.assert_called_once_with( + {"foo": "bar", "good": 25, "bad": 25j}, protocol=mock.ANY + ) def test_set_and_get_service_with_dates__global( self, keychain, key, withdifferentformats diff --git a/cumulusci/tasks/bulkdata/tests/test_step.py b/cumulusci/tasks/bulkdata/tests/test_step.py index ef6854a95f..786c61c819 100644 --- a/cumulusci/tasks/bulkdata/tests/test_step.py +++ b/cumulusci/tasks/bulkdata/tests/test_step.py @@ -1247,7 +1247,7 @@ def test_get_query_operation__inferred_api(self, rest_query, bulk_query): ) assert op == bulk_query.return_value - context.sf.restful.called_once_with("limits/recordCount?sObjects=Test") + context.sf.restful.assert_called_once_with("limits/recordCount?sObjects=Test") @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiDmlOperation") @mock.patch("cumulusci.tasks.bulkdata.step.RestApiDmlOperation") diff --git a/cumulusci/tasks/release_notes/tests/test_task.py b/cumulusci/tasks/release_notes/tests/test_task.py index 45a2d48620..734a925ddc 100644 --- a/cumulusci/tasks/release_notes/tests/test_task.py +++ b/cumulusci/tasks/release_notes/tests/test_task.py @@ -287,8 +287,8 @@ def test_get_child_branch_name_from_merge_commit( get_pr.return_value = [to_return, additional_pull_request] child_branch_name = task._get_child_branch_name_from_merge_commit() assert child_branch_name is None - assert task.logger.error.called_once_with( - "Received multiple pull request,s expected one, for commit sha: {}".format( + task.logger.error.assert_called_once_with( + "Received multiple pull requests, expected one, for commit sha: {}".format( task.commit.sha ) ) @@ -304,7 +304,7 @@ def test_force_option(self, generator, task_factory, gh_api, project_config): generator.return_value = mock.Mock() task._run_task() - assert task.generator.aggregate_child_change_notes.called_once_with( + task.generator.aggregate_child_change_notes.assert_called_once_with( pull_request ) assert not task.generator.update_unaggregated_pr_header.called diff --git a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py index cceac1e68f..6af96ce583 100644 --- a/cumulusci/tasks/salesforce/tests/test_PackageUpload.py +++ b/cumulusci/tasks/salesforce/tests/test_PackageUpload.py @@ -309,7 +309,7 @@ def test_make_package_upload_request(self, datetime): task._make_package_upload_request() assert task._upload_start_time == upload_start_time - assert task.logger.info.called_once_with( + task.logger.info.assert_called_once_with( f"Created PackageUploadRequest {upload_id} for Package {package_id}" ) @@ -347,27 +347,26 @@ def test_handle_apex_test_failures(self): task._handle_apex_test_failures() - assert task.logger.error.called_once_with("Failed Apex Test") + task.logger.error.assert_called_once_with("Failed Apex Tests:") assert task._get_apex_test_results_from_upload.call_count == 1 assert task._log_failures.call_count == 1 - @mock.patch("cumulusci.tasks.salesforce.package_upload.CliTable") + @mock.patch("cumulusci.tasks.salesforce.package_upload.CliTable", autospec=True) def test_log_failures(self, table): - table.echo = mock.Mock() task = create_task(PackageUpload, {"name": "Test Release"}) table_data = [1, 2, 3, 4] - task._get_table_data = mock.Mock(return_value="[1,2,3,4]") + task._get_table_data = mock.Mock(return_value=table_data) results = "Test Results" task._log_failures(results) - assert table.called_once_with( + table.assert_called_once_with( table_data, "Failed Apex Tests", ) - assert table.echo.called_once() + table.return_value.echo.assert_called() def test_get_table_data(self): task = create_task(PackageUpload, {"name": "Test Release"}) @@ -515,7 +514,7 @@ def test_log_package_upload_success(self): task._log_package_upload_success() - assert task.logger.info.called_once_with( + task.logger.info.assert_called_once_with( f"Uploaded package version {version_number} with Id {version_id}" ) diff --git a/cumulusci/tests/test_main.py b/cumulusci/tests/test_main.py index 0c35cc688b..27034fb2ce 100644 --- a/cumulusci/tests/test_main.py +++ b/cumulusci/tests/test_main.py @@ -6,4 +6,4 @@ def test_main(): from cumulusci import __main__ __main__ - assert main.called_once + main.assert_called_once() diff --git a/cumulusci/utils/tests/test_logging.py b/cumulusci/utils/tests/test_logging.py index 3ce5c77771..9a417eade8 100644 --- a/cumulusci/utils/tests/test_logging.py +++ b/cumulusci/utils/tests/test_logging.py @@ -29,7 +29,7 @@ def test_tee_stdout_stderr(self, gist_logger): sys.stdout.write(expected_stdout_text) sys.stderr.write(expected_stderr_text) - assert gist_logger.called_once() + gist_logger.assert_called_once() assert logger.debug.call_count == 3 assert logger.debug.call_args_list[0][0][0] == "cci test\n" assert logger.debug.call_args_list[1][0][0] == expected_stdout_text From fca67fffc37a81e962a625911e9c57f2ef033678 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 26 Oct 2023 14:24:06 -0700 Subject: [PATCH 74/98] Replace deprecated ssl.wrap_socket in OAuth client `ssl.wrap_socket()` has been discouraged since Python 3.2, deprecated since 3.7, and finally removed in 3.12. In this commit, the ssl.wrap_socket function has been removed and replaced with the recommended approach using ssl.SSLContext. Previously, ssl.wrap_socket internally set up the SSL context, including loading certificates. Now, we're doing that explicitly with ssl.SSLContext. On the testing side, the previous workaround for suppressing the CERTIFICATE_VERIFY_FAILED error has been adapted to utilize the context creation method. A note (FIXME): We're currently using ssl.PROTOCOL_TLS for compatibility, but once we drop Python 3.8 support, we should shift to ssl.PROTOCOL_TLS_SERVER, as Python 3.10 no longer includes the generic one. --- cumulusci/oauth/client.py | 12 +++++++----- cumulusci/oauth/tests/test_client.py | 7 ++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cumulusci/oauth/client.py b/cumulusci/oauth/client.py index 3aa42ce623..9d812c7def 100644 --- a/cumulusci/oauth/client.py +++ b/cumulusci/oauth/client.py @@ -197,14 +197,16 @@ def _create_httpd(self): raise if use_https: - if not Path("localhost.pem").is_file() or not Path("key.pem").is_file(): + certfile = "localhost.pem" + keyfile = "key.pem" + if not Path(certfile).is_file() or not Path(keyfile).is_file(): create_key_and_self_signed_cert() - httpd.socket = ssl.wrap_socket( + # FIXME: Use ssl.PROTOCOL_TLS_SERVER after dropping 3.8 support + ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS) + ssl_context.load_cert_chain(certfile, keyfile) + httpd.socket = ssl_context.wrap_socket( httpd.socket, server_side=True, - certfile="localhost.pem", - keyfile="key.pem", - ssl_version=ssl.PROTOCOL_TLS, ) httpd.timeout = self.httpd_timeout diff --git a/cumulusci/oauth/tests/test_client.py b/cumulusci/oauth/tests/test_client.py index 1737792e9a..9f0e939cb2 100644 --- a/cumulusci/oauth/tests/test_client.py +++ b/cumulusci/oauth/tests/test_client.py @@ -156,14 +156,15 @@ def test_auth_code_flow___https(self, client): # use https for callback client.client_config.redirect_uri = "https://localhost:8080/callback" # squash CERTIFICATE_VERIFY_FAILED from urllib - # https://stackoverflow.com/questions/49183801/ssl-certificate-verify-failed-with-urllib - ssl._create_default_https_context = ssl._create_unverified_context + # https://peps.python.org/pep-0476/ + unverified_context = ssl._create_unverified_context() # call OAuth object on another thread - this spawns local httpd with httpd_thread(client) as oauth_client: # simulate callback from browser response = urllib.request.urlopen( - oauth_client.client_config.redirect_uri + "?code=123" + oauth_client.client_config.redirect_uri + "?code=123", + context=unverified_context, ) assert oauth_client.response.json() == expected_response From d2d5493ff83932f21dbc135e24884d5fe4328205 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Wed, 25 Oct 2023 15:12:27 -0700 Subject: [PATCH 75/98] Update CI and project metadata for 3.12 --- .github/workflows/feature_test.yml | 5 +---- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/feature_test.yml b/.github/workflows/feature_test.yml index 7f678f234a..03a532a911 100644 --- a/.github/workflows/feature_test.yml +++ b/.github/workflows/feature_test.yml @@ -41,10 +41,7 @@ jobs: fail-fast: false matrix: os: [macos-latest, sfdc-ubuntu-latest, sfdc-windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] - exclude: # FIXME: Remove when lxml publishes a 311-windows wheel - - os: sfdc-windows-latest - python-version: "3.11" + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python diff --git a/pyproject.toml b/pyproject.toml index 5a53cb15e3..1644042640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "click", From 6e199326ed960b9278c657ee7c1d88a12780c56d Mon Sep 17 00:00:00 2001 From: James Estevez Date: Wed, 25 Oct 2023 15:13:24 -0700 Subject: [PATCH 76/98] Switch to the sf CLI in unit tests --- .github/workflows/feature_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/feature_test.yml b/.github/workflows/feature_test.yml index 03a532a911..3758e4ea6b 100644 --- a/.github/workflows/feature_test.yml +++ b/.github/workflows/feature_test.yml @@ -71,7 +71,7 @@ jobs: - name: Install sfdx run: | mkdir sfdx - wget -qO- https://developer.salesforce.com/media/salesforce-cli/sfdx/channels/stable/sfdx-linux-x64.tar.xz | tar xJ -C sfdx --strip-components 1 + wget -qO- https://developer.salesforce.com/media/salesforce-cli/sf/channels/stable/sf-linux-x64.tar.xz | tar xJ -C sfdx --strip-components 1 echo $(realpath sfdx/bin) >> $GITHUB_PATH - name: Authenticate Dev Hub run: | From 126d7909ce0cd5d6a471aa1bfef450204252d0d0 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Wed, 15 Nov 2023 04:25:27 +0530 Subject: [PATCH 77/98] Make task `options` read only when using Pydantic option validation (#3695) [W-13798912](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001WfO3eYAF/view) Made `self.options` to be Read-Only, if an `Options` class is defined in the Task. Added usage to documentation. Additionally, corrected `ListMetadataTypes` in `cumulusci/tasks/util.py` to use `parsed_options` instead of `options` (was missed in the pydantic options commit). Fixes: #3627 --------- Co-authored-by: James Estevez Co-authored-by: David Reed --- cumulusci/core/tasks.py | 3 +- cumulusci/tasks/util.py | 8 ++--- cumulusci/utils/options.py | 21 +++++++++++++ cumulusci/utils/tests/test_option_parsing.py | 32 ++++++++++++++++++++ docs/config.md | 4 +++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/cumulusci/core/tasks.py b/cumulusci/core/tasks.py index e9f5b2f115..0d8269aea4 100644 --- a/cumulusci/core/tasks.py +++ b/cumulusci/core/tasks.py @@ -28,7 +28,7 @@ from cumulusci.utils import cd from cumulusci.utils.logging import redirect_output_to_logger from cumulusci.utils.metaprogramming import classproperty -from cumulusci.utils.options import CCIOptions +from cumulusci.utils.options import CCIOptions, ReadOnlyOptions CURRENT_TASK = threading.local() @@ -163,6 +163,7 @@ def process_options(option): opt: val for opt, val in self.options.items() if opt not in specials } self.parsed_options = self.Options(**options_without_specials) + self.options = ReadOnlyOptions(self.options) except ValidationError as e: try: errors = [ diff --git a/cumulusci/tasks/util.py b/cumulusci/tasks/util.py index afb574dfca..50d43ce827 100644 --- a/cumulusci/tasks/util.py +++ b/cumulusci/tasks/util.py @@ -59,13 +59,13 @@ class Options(CCIOptions): def _init_options(self, kwargs): super(ListMetadataTypes, self)._init_options(kwargs) - if not self.options.get("package_xml"): - self.options["package_xml"] = os.path.join( + if not self.parsed_options.get("package_xml"): + self.parsed_options["package_xml"] = os.path.join( self.project_config.repo_root, "src", "package.xml" ) def _run_task(self): - dom = parse(self.options["package_xml"]) + dom = parse(self.parsed_options["package_xml"]) package = dom.getElementsByTagName("Package")[0] types = package.getElementsByTagName("types") type_list = [] @@ -75,7 +75,7 @@ def _run_task(self): type_list.append(metadata_type) self.logger.info( "Metadata types found in %s:\r\n%s", - self.options["package_xml"], + self.parsed_options["package_xml"], "\r\n".join(type_list), ) diff --git a/cumulusci/utils/options.py b/cumulusci/utils/options.py index 3f9fe11d94..5d5d2828f0 100644 --- a/cumulusci/utils/options.py +++ b/cumulusci/utils/options.py @@ -4,8 +4,13 @@ from pydantic import DirectoryPath, Field, FilePath, create_model +from cumulusci.core.exceptions import TaskOptionsError from cumulusci.utils.yaml.model_parser import CCIDictModel +READONLYDICT_ERROR_MSG = ( + "The 'options' dictionary is read-only. Please use 'parsed_options' instead." +) + def _describe_field(field): "Convert a Pydantic field into a CCI task_option dict" @@ -18,6 +23,22 @@ def _describe_field(field): return rc +class ReadOnlyOptions(dict): + """To enforce self.options to be read-only""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __setitem__(self, key, value): + raise TaskOptionsError(READONLYDICT_ERROR_MSG) + + def __delitem__(self, key): + raise TaskOptionsError(READONLYDICT_ERROR_MSG) + + def pop(self, key, default=None): + raise TaskOptionsError(READONLYDICT_ERROR_MSG) + + class CCIOptions(CCIDictModel): "Base class for all options in tasks" diff --git a/cumulusci/utils/tests/test_option_parsing.py b/cumulusci/utils/tests/test_option_parsing.py index 82fceb962c..d36dd44dfa 100644 --- a/cumulusci/utils/tests/test_option_parsing.py +++ b/cumulusci/utils/tests/test_option_parsing.py @@ -12,10 +12,12 @@ from cumulusci.core.exceptions import TaskOptionsError from cumulusci.core.tasks import BaseTask from cumulusci.utils.options import ( + READONLYDICT_ERROR_MSG, CCIOptions, Field, ListOfStringsOption, MappingOption, + ReadOnlyOptions, ) ORG_ID = "00D000000000001" @@ -45,6 +47,10 @@ def _run_task(self): print(key, repr(getattr(self.parsed_options, key))) +class TaskWithoutOptions(BaseTask): + pass + + class TestTaskOptionsParsing: def setup_class(self): self.global_config = UniversalConfig() @@ -154,3 +160,29 @@ def test_multiple_errors(self): assert "the_bool" in str(e.value) assert "req" in str(e.value) assert "Errors" in str(e.value) + + def test_options_read_only(self): + # Has an Options class + task1 = TaskToTestTypes(self.project_config, self.task_config, self.org_config) + assert isinstance(task1.options, ReadOnlyOptions) + # Does not have an Options class + task2 = TaskWithoutOptions( + self.project_config, self.task_config, self.org_config + ) + assert isinstance(task2.options, dict) + + def test_init_options__options_read_only_error(self): + expected_error_msg = READONLYDICT_ERROR_MSG + task = TaskToTestTypes(self.project_config, self.task_config, self.org_config) + # Add new option + with pytest.raises(TaskOptionsError, match=expected_error_msg): + task.options["new_option"] = "something" + # Modify existing option + with pytest.raises(TaskOptionsError, match=expected_error_msg): + task.options["test_option"] = 456 + # Delete existing option + with pytest.raises(TaskOptionsError, match=expected_error_msg): + del task.options["test_option"] + # Pop existing option + with pytest.raises(TaskOptionsError, match=expected_error_msg): + task.options.pop("test_option") diff --git a/docs/config.md b/docs/config.md index 12e3db1e8e..99ca4fc416 100644 --- a/docs/config.md +++ b/docs/config.md @@ -176,6 +176,10 @@ and (2) A required file path. Once the options are defined, they can be accessed via the `parsed_options` property of the task. +```{important} +When the nested `Options` class is defined within your custom task (or is part of a class you inherit from), it restricts modifications to the `options` property of the task, making it read-only. To make any changes, you should instead modify the `parsed_options` property rather than the `options` property. +``` + Some of the most commonly used types are: - `pathlib.Path`: simply uses the type itself for validation by passing the value to Path(v); From dd72107a4a1e90c03679f370418193f666dd7999 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:24:36 -0700 Subject: [PATCH 78/98] w48 dependency updates (automated) (#3710) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: James Estevez --- requirements/dev.txt | 51 ++++++++++++++++++++++++------------------- requirements/prod.txt | 19 +++++++++++----- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index c5f76d24ed..59bd22d4cf 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -15,13 +15,13 @@ attrs==23.1.0 # referencing authlib==1.2.1 # via simple-salesforce -babel==2.13.0 +babel==2.13.1 # via sphinx beautifulsoup4==4.12.2 # via furo -black==23.10.0 +black==23.11.0 # via cumulusci (pyproject.toml) -cachetools==5.3.1 +cachetools==5.3.2 # via tox certifi==2023.7.22 # via @@ -48,11 +48,12 @@ coverage[toml]==7.3.2 # via # cumulusci (pyproject.toml) # pytest-cov -cryptography==41.0.4 +cryptography==41.0.7 # via # authlib # cumulusci (pyproject.toml) # pyjwt + # secretstorage defusedxml==0.7.1 # via cumulusci (pyproject.toml) distlib==0.3.7 @@ -75,7 +76,7 @@ faker-edu==1.0.0 # via snowfakery faker-nonprofit==1.0.0 # via snowfakery -filelock==3.12.4 +filelock==3.13.1 # via # tox # virtualenv @@ -87,11 +88,11 @@ furo==2023.3.27 # via cumulusci (pyproject.toml) github3-py==4.0.1 # via cumulusci (pyproject.toml) -greenlet==3.0.0 +greenlet==3.0.1 # via sqlalchemy gvgen==1.0 # via snowfakery -identify==2.5.30 +identify==2.5.32 # via pre-commit idna==3.4 # via @@ -104,7 +105,7 @@ importlib-metadata==6.8.0 # via # keyring # sphinx -importlib-resources==6.1.0 +importlib-resources==6.1.1 # via # jsonschema # jsonschema-specifications @@ -112,15 +113,19 @@ iniconfig==2.0.0 # via pytest isort==5.12.0 # via cumulusci (pyproject.toml) +jeepney==0.8.0 + # via + # keyring + # secretstorage jinja2==3.1.2 # via # cumulusci (pyproject.toml) # myst-parser # snowfakery # sphinx -jsonschema==4.19.1 +jsonschema==4.20.0 # via cumulusci (pyproject.toml) -jsonschema-specifications==2023.7.1 +jsonschema-specifications==2023.11.2 # via jsonschema keyring==23.0.1 # via cumulusci (pyproject.toml) @@ -164,7 +169,7 @@ pathspec==0.11.2 # via black pkgutil-resolve-name==1.3.10 # via jsonschema -platformdirs==3.11.0 +platformdirs==4.0.0 # via # black # tox @@ -189,7 +194,7 @@ pydantic==1.10.12 # snowfakery pyflakes==2.3.1 # via flake8 -pygments==2.16.1 +pygments==2.17.2 # via # furo # rich @@ -232,7 +237,7 @@ pyyaml==6.0.1 # responses # snowfakery # vcrpy -referencing==0.30.2 +referencing==0.31.1 # via # jsonschema # jsonschema-specifications @@ -251,7 +256,7 @@ requests-futures==1.0.1 # via cumulusci (pyproject.toml) responses==0.23.1 # via cumulusci (pyproject.toml) -rich==13.6.0 +rich==13.7.0 # via cumulusci (pyproject.toml) robotframework==6.1.1 # via @@ -262,15 +267,15 @@ robotframework==6.1.1 # robotframework-stacktrace robotframework-pabot==2.16.0 # via cumulusci (pyproject.toml) -robotframework-pythonlibcore==4.2.0 +robotframework-pythonlibcore==4.3.0 # via robotframework-seleniumlibrary -robotframework-requests==0.9.5 +robotframework-requests==0.9.6 # via cumulusci (pyproject.toml) robotframework-seleniumlibrary==5.1.3 # via cumulusci (pyproject.toml) robotframework-stacktrace==0.4.1 # via robotframework-pabot -rpds-py==0.10.6 +rpds-py==0.13.2 # via # jsonschema # referencing @@ -280,6 +285,8 @@ salesforce-bulk==2.2.0 # via cumulusci (pyproject.toml) sarge==0.1.7.post1 # via cumulusci (pyproject.toml) +secretstorage==3.3.3 + # via keyring selenium==3.141.0 # via # cumulusci (pyproject.toml) @@ -324,7 +331,7 @@ sqlalchemy==1.4.49 # via # cumulusci (pyproject.toml) # snowfakery -testfixtures==7.2.0 +testfixtures==7.2.2 # via cumulusci (pyproject.toml) tomli==2.0.1 # via @@ -333,7 +340,7 @@ tomli==2.0.1 # pyproject-api # pytest # tox -tox==4.11.3 +tox==4.11.4 # via cumulusci (pyproject.toml) typeguard==2.13.3 # via cumulusci (pyproject.toml) @@ -361,15 +368,15 @@ vcrpy==5.1.0 # via # cumulusci (pyproject.toml) # pytest-vcr -virtualenv==20.24.5 +virtualenv==20.24.7 # via # pre-commit # tox -wrapt==1.15.0 +wrapt==1.16.0 # via vcrpy xmltodict==0.13.0 # via cumulusci (pyproject.toml) -yarl==1.9.2 +yarl==1.9.3 # via vcrpy zipp==3.17.0 # via diff --git a/requirements/prod.txt b/requirements/prod.txt index 3939c539ea..151f818426 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -22,11 +22,12 @@ click==8.1.6 # via # cumulusci (pyproject.toml) # snowfakery -cryptography==41.0.4 +cryptography==41.0.7 # via # authlib # cumulusci (pyproject.toml) # pyjwt + # secretstorage defusedxml==0.7.1 # via cumulusci (pyproject.toml) docutils==0.16 @@ -45,7 +46,7 @@ fs==2.4.16 # via cumulusci (pyproject.toml) github3-py==4.0.1 # via cumulusci (pyproject.toml) -greenlet==3.0.0 +greenlet==3.0.1 # via sqlalchemy gvgen==1.0 # via snowfakery @@ -55,6 +56,10 @@ idna==3.4 # snowfakery importlib-metadata==6.8.0 # via keyring +jeepney==0.8.0 + # via + # keyring + # secretstorage jinja2==3.1.2 # via # cumulusci (pyproject.toml) @@ -84,7 +89,7 @@ pydantic==1.10.12 # via # cumulusci (pyproject.toml) # snowfakery -pygments==2.16.1 +pygments==2.17.2 # via rich pyjwt[crypto]==2.8.0 # via @@ -115,7 +120,7 @@ requests==2.29.0 # snowfakery requests-futures==1.0.1 # via cumulusci (pyproject.toml) -rich==13.6.0 +rich==13.7.0 # via cumulusci (pyproject.toml) robotframework==6.1.1 # via @@ -126,9 +131,9 @@ robotframework==6.1.1 # robotframework-stacktrace robotframework-pabot==2.16.0 # via cumulusci (pyproject.toml) -robotframework-pythonlibcore==4.2.0 +robotframework-pythonlibcore==4.3.0 # via robotframework-seleniumlibrary -robotframework-requests==0.9.5 +robotframework-requests==0.9.6 # via cumulusci (pyproject.toml) robotframework-seleniumlibrary==5.1.3 # via cumulusci (pyproject.toml) @@ -140,6 +145,8 @@ salesforce-bulk==2.2.0 # via cumulusci (pyproject.toml) sarge==0.1.7.post1 # via cumulusci (pyproject.toml) +secretstorage==3.3.3 + # via keyring selenium==3.141.0 # via # cumulusci (pyproject.toml) From 9b11bb82db167e1558b74c22637b897e9ec17fe2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:24:08 -0700 Subject: [PATCH 79/98] Release v3.82.0 (#3712) --- cumulusci/__about__.py | 2 +- docs/history.md | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 58dff8fea3..56b944aaa3 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.81.0" +__version__ = "3.82.0" diff --git a/docs/history.md b/docs/history.md index 1aa01d973c..16204b4b92 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,36 @@ +## v3.82.0 (2023-12-01) + + + +## What's Changed + +### Critical Changes 🛠 + +- Remove `robot_lint` task and dependencies by [@jstvz](https://github.com/jstvz) in [#3697](https://github.com/SFDO-Tooling/CumulusCI/pull/3697) + +### Changes 🎉 + +- Show an error message when no `origin` remote is present by [@lakshmi2506](https://github.com/lakshmi2506) in [#3679](https://github.com/SFDO-Tooling/CumulusCI/pull/3679) +- Improve task `return_values` documentation by [@lakshmi2506](https://github.com/lakshmi2506) in [#3689](https://github.com/SFDO-Tooling/CumulusCI/pull/3689) +- Task to retrieve a complete Profile from an org by [@aditya-balachander](https://github.com/aditya-balachander) in [#3672](https://github.com/SFDO-Tooling/CumulusCI/pull/3672) +- Support Python 3.12 by [@jstvz](https://github.com/jstvz) in [#3691](https://github.com/SFDO-Tooling/CumulusCI/pull/3691) +- Make task `options` read only when using Pydantic option validation by [@aditya-balachander](https://github.com/aditya-balachander) in [#3695](https://github.com/SFDO-Tooling/CumulusCI/pull/3695) + +### Issues Fixed 🩴 + +- Fix Github url parse error for some scenarios by [@mgrandhi](https://github.com/mgrandhi) in [#3683](https://github.com/SFDO-Tooling/CumulusCI/pull/3683) + +## New Contributors + +- @mgrandhi made their first contribution in [#3683](https://github.com/SFDO-Tooling/CumulusCI/pull/3683) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.81.0...v3.82.0 + + + ## v3.81.0 (2023-11-03) @@ -21,8 +51,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.80.0...v3.81.0 - - ## v3.80.0 (2023-09-29) From 7b8d84647f0dad278373b27cd9b9cde09b498965 Mon Sep 17 00:00:00 2001 From: Naman Jain Date: Wed, 6 Dec 2023 02:39:09 +0530 Subject: [PATCH 80/98] Allowed namespace injection without managed (#3677) Co-authored-by: James Estevez --- cumulusci/tasks/apex/testrunner.py | 7 +++++++ cumulusci/tasks/apex/tests/test_apex_tasks.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/cumulusci/tasks/apex/testrunner.py b/cumulusci/tasks/apex/testrunner.py index 468f3d73ac..5a51f655ed 100644 --- a/cumulusci/tasks/apex/testrunner.py +++ b/cumulusci/tasks/apex/testrunner.py @@ -262,13 +262,19 @@ def _init_class(self): self.retry_details = None def _get_namespace_filter(self): + if self.options.get("managed"): + namespace = self.options.get("namespace") + if not namespace: raise TaskOptionsError( "Running tests in managed mode but no namespace available." ) namespace = "'{}'".format(namespace) + elif self.org_config.namespace: + namespace = self.org_config.namespace + namespace = "'{}'".format(namespace) else: namespace = "null" return namespace @@ -291,6 +297,7 @@ def _get_test_class_query(self): query = "SELECT Id, Name FROM ApexClass " + "WHERE NamespacePrefix = {}".format( namespace ) + if included_tests: query += " AND ({})".format(" OR ".join(included_tests)) if excluded_tests: diff --git a/cumulusci/tasks/apex/tests/test_apex_tasks.py b/cumulusci/tasks/apex/tests/test_apex_tasks.py index 2d312a8f48..7c9079310b 100644 --- a/cumulusci/tasks/apex/tests/test_apex_tasks.py +++ b/cumulusci/tasks/apex/tests/test_apex_tasks.py @@ -867,6 +867,21 @@ def test_get_namespace_filter__managed(self): namespace = task._get_namespace_filter() assert namespace == "'testns'" + def test_get_namespace_filter__target_org(self): + task_config = TaskConfig({"options": {}}) + org_config = OrgConfig( + { + "id": "foo/1", + "instance_url": "https://example.com", + "access_token": "abc123", + "namespace": "testns", + }, + "test", + ) + task = RunApexTests(self.project_config, task_config, org_config) + namespace = task._get_namespace_filter() + assert namespace == "'testns'" + def test_get_namespace_filter__managed_no_namespace(self): task_config = TaskConfig({"options": {"managed": True}}) task = RunApexTests(self.project_config, task_config, self.org_config) From c2350d18492c41e67a50f4ae8b23fa1542c3090e Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Wed, 6 Dec 2023 11:30:30 +0530 Subject: [PATCH 81/98] handled incorrect tablename --- .../tasks/bulkdata/query_transformers.py | 19 +++++++++++++++---- .../tasks/bulkdata/tests/recordtypes_2.sql | 19 +++++++++++++++++++ .../tasks/bulkdata/tests/recordtypes_2.yml | 6 ++++++ cumulusci/tasks/bulkdata/tests/test_load.py | 12 ++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 cumulusci/tasks/bulkdata/tests/recordtypes_2.sql create mode 100644 cumulusci/tasks/bulkdata/tests/recordtypes_2.yml diff --git a/cumulusci/tasks/bulkdata/query_transformers.py b/cumulusci/tasks/bulkdata/query_transformers.py index b3e636636b..d37604892d 100644 --- a/cumulusci/tasks/bulkdata/query_transformers.py +++ b/cumulusci/tasks/bulkdata/query_transformers.py @@ -103,16 +103,27 @@ def filters_to_add(self): @cached_property def outerjoins_to_add(self): + if "RecordTypeId" in self.mapping.fields: try: rt_source_table = self.metadata.tables[ self.mapping.get_source_record_type_table() ] + except KeyError as e: - raise BulkDataException( - "A record type mapping table was not found in your dataset. " - f"Was it generated by extract_data? {e}", - ) from e + + try: + rt_source_table = self.metadata.tables[ + f"{self.mapping.table}_rt_mapping" + ] + + except KeyError as f: + + raise BulkDataException( + "A record type mapping table was not found in your dataset. " + f"Was it generated by extract_data? {e}", + ) from f + rt_dest_table = self.metadata.tables[ self.mapping.get_destination_record_type_table() ] diff --git a/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql b/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql new file mode 100644 index 0000000000..8b39829847 --- /dev/null +++ b/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql @@ -0,0 +1,19 @@ +BEGIN TRANSACTION; +CREATE TABLE Beta_rt_mapping ( + record_type_id VARCHAR(18) NOT NULL, + developer_name VARCHAR(255), + PRIMARY KEY (record_type_id) +); +INSERT INTO "Beta_rt_mapping" VALUES('012H40000003jCoIAI','recordtype2'); +INSERT INTO "Beta_rt_mapping" VALUES('012H40000003jCZIAY','recordtype1'); +CREATE TABLE Beta ( + id INTEGER NOT NULL, + "Name" VARCHAR(255), + "RecordType" VARCHAR(255), + PRIMARY KEY (id) +); + +INSERT INTO "Beta" VALUES(15,'gamma12','012H40000003jCoIAI'); +INSERT INTO "Beta" VALUES(16,'gamma','012H40000003jCZIAY'); +INSERT INTO "Beta" VALUES(17,'gamma123','012H40000003jCZIAY'); +COMMIT; diff --git a/cumulusci/tasks/bulkdata/tests/recordtypes_2.yml b/cumulusci/tasks/bulkdata/tests/recordtypes_2.yml new file mode 100644 index 0000000000..426c2ee50e --- /dev/null +++ b/cumulusci/tasks/bulkdata/tests/recordtypes_2.yml @@ -0,0 +1,6 @@ +Insert Account: + sf_object: Account + table: Beta + fields: + Name: Name + RecordTypeId: RecordType diff --git a/cumulusci/tasks/bulkdata/tests/test_load.py b/cumulusci/tasks/bulkdata/tests/test_load.py index 23ec22c543..4609a0320d 100644 --- a/cumulusci/tasks/bulkdata/tests/test_load.py +++ b/cumulusci/tasks/bulkdata/tests/test_load.py @@ -1666,6 +1666,18 @@ def test_query_db__record_type_mapping(self): """, ) + def test_query_db__record_type_mapping_table_from_tablename(self): + _validate_query_for_mapping_step( + sql_path="cumulusci/tasks/bulkdata/tests/recordtypes_2.sql", + mapping="cumulusci/tasks/bulkdata/tests/recordtypes_2.yml", + mapping_step_name="Insert Account", + expected="""SELECT "Beta".id AS "Beta_id", "Beta"."Name" AS "Beta_Name", "Account_rt_target_mapping".record_type_id AS "Account_rt_target_mapping_record_type_id" + FROM "Beta" + LEFT OUTER JOIN "Beta_rt_mapping" ON "Beta_rt_mapping".record_type_id = "Beta"."RecordType" + LEFT OUTER JOIN "Account_rt_target_mapping" ON "Account_rt_target_mapping".developer_name = "Beta_rt_mapping".developer_name + """, + ) + @mock.patch("cumulusci.tasks.bulkdata.load.automap_base") @responses.activate def test_init_db__record_type_mapping(self, base): From 68149d8722ab99c942a0dcf4d560f9d481f423a1 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 7 Dec 2023 18:43:09 -0800 Subject: [PATCH 82/98] Update pyright configuration for 1.1.339 (#3715) Pyright changed the default typechecking level from 'basic' to 'standard' in v1.1.339. We don't have time to fix these right now, so this commit explicitly declares our `typeCheckingMode` to 'basic' until we do. Also, our pre-commit configuration for pyright needs to be improved. First, pyright can't see find our imports because pre-commit installs hooks in their own environments. We could: 1. Fix our local pre-commit hook so it uses the global environment, 2. Install our dependencies into the hook environment, 3. Ignore missing imports until we do (1) or (2). This commit chooses (3). Second, using a local hook for Pyright results in inconsistencies in the version used across different environments (local development vs CI). Although an official hook is not available, 'python-pyright' was considered. However, it presents similar challenges in module detection. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1644042640..1d94df527f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,8 @@ known_first_party = "cumulusci" known_third_party = "robot" [tool.pyright] -reportMissingImports = "warning" +reportMissingImports = "none" +typeCheckingMode = "basic" exclude = ["**/test_*", "**/tests/**"] # Add files to this list as you make them compatible. include = [ @@ -234,7 +235,6 @@ include = [ 'cumulusci/tasks/robotframework/__init__.py', 'cumulusci/tasks/robotframework/debugger/__init__.py', 'cumulusci/tasks/robotframework/debugger/model.py', - 'cumulusci/tasks/robotframework/lint.py', 'cumulusci/tasks/salesforce/BaseRetrieveMetadata.py', 'cumulusci/tasks/salesforce/BaseSalesforceTask.py', 'cumulusci/tasks/salesforce/GetInstalledPackages.py', From eec688617da58dadc7a936b367b44e0832c45167 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 7 Dec 2023 18:56:16 -0800 Subject: [PATCH 83/98] Replace distutils with shutil for 3.12 (#3714) This PR replaces distutils.dir_util.copy_tree(update=1) with a custom `update_tree` function using `shutil` because Python 3.12 removed `distutils`. Our tests did not catch this issue because our test environments include `setuptools`, which vendors `distutils` Added unit tests to ensure update_tree only copies new or missing files, maintaining the original behavior in those tasks that used `copy_tree`. Fixes #3707 --- cumulusci/tasks/metadata/ee_src.py | 12 ++-- cumulusci/tasks/metadata/managed_src.py | 11 ++- cumulusci/tasks/metadata/tests/test_ee_src.py | 67 ++++++++++++++----- .../tasks/metadata/tests/test_managed_src.py | 67 ++++++++++++++----- cumulusci/utils/__init__.py | 30 +++++++++ cumulusci/utils/tests/test_fileutils.py | 31 ++++++++- 6 files changed, 172 insertions(+), 46 deletions(-) diff --git a/cumulusci/tasks/metadata/ee_src.py b/cumulusci/tasks/metadata/ee_src.py index fab12dc558..a35d961d83 100644 --- a/cumulusci/tasks/metadata/ee_src.py +++ b/cumulusci/tasks/metadata/ee_src.py @@ -1,11 +1,9 @@ import os - -# TODO: Replace this with shutils -from distutils.dir_util import copy_tree, remove_tree +from shutil import copytree, rmtree from cumulusci.core.exceptions import TaskOptionsError from cumulusci.core.tasks import BaseTask -from cumulusci.utils import remove_xml_element_directory +from cumulusci.utils import remove_xml_element_directory, update_tree class CreateUnmanagedEESrc(BaseTask): @@ -40,7 +38,7 @@ def _run_task(self): ) # Copy path to revert_path - copy_tree(self.options["path"], self.options["revert_path"]) + copytree(self.options["path"], self.options["revert_path"]) # Edit metadata in path self.logger.info( @@ -88,9 +86,9 @@ def _run_task(self): self.options["path"], self.options["revert_path"] ) ) - copy_tree(self.options["revert_path"], self.options["path"], update=1) + update_tree(self.options["revert_path"], self.options["path"]) self.logger.info("{} is now reverted".format(self.options["path"])) # Delete the revert_path self.logger.info("Deleting {}".format(self.options["revert_path"])) - remove_tree(self.options["revert_path"]) + rmtree(self.options["revert_path"]) diff --git a/cumulusci/tasks/metadata/managed_src.py b/cumulusci/tasks/metadata/managed_src.py index d51a75ff0d..06f0479bd3 100644 --- a/cumulusci/tasks/metadata/managed_src.py +++ b/cumulusci/tasks/metadata/managed_src.py @@ -1,10 +1,9 @@ -# TODO: Replace this with shutils -from distutils.dir_util import copy_tree, remove_tree from pathlib import Path +from shutil import copytree, rmtree from cumulusci.core.exceptions import TaskOptionsError from cumulusci.core.tasks import BaseTask -from cumulusci.utils import find_replace +from cumulusci.utils import find_replace, update_tree class CreateManagedSrc(BaseTask): @@ -45,7 +44,7 @@ def _run_task(self): ) # Copy path to revert_path - copy_tree(str(path), str(revert_path)) + copytree(str(path), str(revert_path)) # Edit metadata in path self.logger.info( @@ -93,9 +92,9 @@ def _run_task(self): ) self.logger.info(f"Reverting {path} from {revert_path}") - copy_tree(str(revert_path), str(path), update=1) + update_tree(str(revert_path), str(path)) self.logger.info(f"{path} is now reverted") # Delete the revert_path self.logger.info(f"Deleting {str(revert_path)}") - remove_tree(revert_path) + rmtree(revert_path) diff --git a/cumulusci/tasks/metadata/tests/test_ee_src.py b/cumulusci/tasks/metadata/tests/test_ee_src.py index 7455853df9..b8d6ba81bc 100644 --- a/cumulusci/tasks/metadata/tests/test_ee_src.py +++ b/cumulusci/tasks/metadata/tests/test_ee_src.py @@ -1,4 +1,5 @@ import os +import time from unittest import mock import pytest @@ -52,22 +53,21 @@ def test_run_task__revert_path_already_exists(self): class TestRevertUnmanagedEESrc: - def test_run_task(self): - with temporary_dir() as revert_path: - with open(os.path.join(revert_path, "file"), "w"): - pass - path = os.path.join( - os.path.dirname(revert_path), os.path.basename(revert_path) + "_orig" - ) - project_config = BaseProjectConfig( - UniversalConfig(), config={"noyaml": True} - ) - task_config = TaskConfig( - {"options": {"path": path, "revert_path": revert_path}} - ) - task = RevertUnmanagedEESrc(project_config, task_config) - task() - assert os.path.exists(os.path.join(path, "file")) + def test_run_task(self, tmp_path): + revert_path = tmp_path / "revert" + revert_path.mkdir() + file_path = revert_path / "file" + file_path.write_text("content") + + path = tmp_path / "path" + path.mkdir() + project_config = BaseProjectConfig(UniversalConfig(), config={"noyaml": True}) + task_config = TaskConfig( + {"options": {"path": str(path), "revert_path": str(revert_path)}} + ) + task = RevertUnmanagedEESrc(project_config, task_config) + task() + assert (path / "file").exists() def test_run_task__revert_path_not_found(self): project_config = BaseProjectConfig(UniversalConfig(), config={"noyaml": True}) @@ -75,3 +75,38 @@ def test_run_task__revert_path_not_found(self): task = RevertUnmanagedEESrc(project_config, task_config) with pytest.raises(TaskOptionsError): task() + + def test_revert_with_update(self, tmp_path): + """ + Test the 'update' behavior of RevertUnmanagedEESrc task with temporary directories. + + This test creates a source and a destination directory each with one + file. The file in the source directory has an older timestamp. After + running RevertUnmanagedEESrc, it checks that the destination file is not + overwritten by the older source file, confirming the update logic. + """ + source_dir = tmp_path / "source" + source_dir.mkdir() + source_file = source_dir / "testfile.txt" + source_file.write_text("original content") + + dest_dir = tmp_path / "dest" + dest_dir.mkdir() + dest_file = dest_dir / "testfile.txt" + dest_file.write_text("modified content") + + # Ensure the source file has an older timestamp + past_time = time.time() - 100 + # Use os.utime to modify the timestamp + source_file.touch() + os.utime(str(source_file), (past_time, past_time)) + + project_config = BaseProjectConfig(UniversalConfig(), config={"noyaml": True}) + task_config = TaskConfig( + {"options": {"path": str(dest_dir), "revert_path": str(source_dir)}} + ) + task = RevertUnmanagedEESrc(project_config, task_config) + task() + + # Verify that the destination file was not updated (due to older source file) + assert dest_file.read_text() == "modified content" diff --git a/cumulusci/tasks/metadata/tests/test_managed_src.py b/cumulusci/tasks/metadata/tests/test_managed_src.py index ffc044c560..670d0ec26a 100644 --- a/cumulusci/tasks/metadata/tests/test_managed_src.py +++ b/cumulusci/tasks/metadata/tests/test_managed_src.py @@ -1,4 +1,5 @@ import os +import time import pytest @@ -51,22 +52,21 @@ def test_run_task__revert_path_already_exists(self): class TestRevertManagedSrc: - def test_run_task(self): - with temporary_dir() as revert_path: - with open(os.path.join(revert_path, "file"), "w"): - pass - path = os.path.join( - os.path.dirname(revert_path), os.path.basename(revert_path) + "_orig" - ) - project_config = BaseProjectConfig( - UniversalConfig(), config={"noyaml": True} - ) - task_config = TaskConfig( - {"options": {"path": path, "revert_path": revert_path}} - ) - task = RevertManagedSrc(project_config, task_config) - task() - assert os.path.exists(os.path.join(path, "file")) + def test_run_task(self, tmp_path): + revert_path = tmp_path / "revert" + revert_path.mkdir() + file_path = revert_path / "file" + file_path.write_text("content") + + path = tmp_path / "path" + path.mkdir() + project_config = BaseProjectConfig(UniversalConfig(), config={"noyaml": True}) + task_config = TaskConfig( + {"options": {"path": str(path), "revert_path": str(revert_path)}} + ) + task = RevertManagedSrc(project_config, task_config) + task() + assert (path / "file").exists() def test_run_task__revert_path_not_found(self): project_config = BaseProjectConfig(UniversalConfig(), config={"noyaml": True}) @@ -74,3 +74,38 @@ def test_run_task__revert_path_not_found(self): task = RevertManagedSrc(project_config, task_config) with pytest.raises(TaskOptionsError): task() + + def test_revert_with_update(self, tmp_path): + """ + Test the 'update' behavior of RevertManagedSrc task with temporary directories. + + This test creates a source and a destination directory each with one + file. The file in the source directory has an older timestamp. After + running RevertManagedSrc, it checks that the destination file is not + overwritten by the older source file, confirming the update logic. + """ + source_dir = tmp_path / "source" + source_dir.mkdir() + source_file = source_dir / "testfile.txt" + source_file.write_text("original content") + + dest_dir = tmp_path / "dest" + dest_dir.mkdir() + dest_file = dest_dir / "testfile.txt" + dest_file.write_text("modified content") + + # Ensure the source file has an older timestamp + past_time = time.time() - 100 + # Use os.utime to modify the timestamp + source_file.touch() + os.utime(str(source_file), (past_time, past_time)) + + project_config = BaseProjectConfig(UniversalConfig(), config={"noyaml": True}) + task_config = TaskConfig( + {"options": {"path": str(dest_dir), "revert_path": str(source_dir)}} + ) + task = RevertManagedSrc(project_config, task_config) + task() + + # Verify that the destination file was not updated (due to older source file) + assert dest_file.read_text() == "modified content" diff --git a/cumulusci/utils/__init__.py b/cumulusci/utils/__init__.py index 605818d96c..2d740c40cb 100644 --- a/cumulusci/utils/__init__.py +++ b/cumulusci/utils/__init__.py @@ -10,6 +10,8 @@ import textwrap import zipfile from datetime import datetime +from pathlib import Path +from typing import Union import requests import sarge @@ -630,3 +632,31 @@ def get_git_config(config_key): ) return config_value if config_value and not p.returncode else None + + +def update_tree(src: Union[str, Path], dest: Union[str, Path]): + """ + Copies files from src to dest, same as distutils.copy_tree(update=1). + + Copies the entire directory tree from src to dest. If dest exists, only + copies files that are newer in src than in dest, or files that don't exist + in dest. + + Args: + src (Union[str, Path]): The source directory to copy files from. + dest (Union[str, Path]): The destination directory to copy files to. + """ + src_path = Path(src) + dest_path = Path(dest) + if not dest_path.exists(): + shutil.copytree(src_path, dest_path) + else: + for src_dir in src_path.rglob("*"): + if src_dir.is_file(): + dest_file = dest_path / src_dir.relative_to(src_path) + if ( + not dest_file.exists() + or src_dir.stat().st_mtime - dest_file.stat().st_mtime > 1 + ): + dest_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src_dir, dest_file) diff --git a/cumulusci/utils/tests/test_fileutils.py b/cumulusci/utils/tests/test_fileutils.py index e5bf70c9ca..7b09afd2dd 100644 --- a/cumulusci/utils/tests/test_fileutils.py +++ b/cumulusci/utils/tests/test_fileutils.py @@ -1,6 +1,7 @@ import doctest import os import sys +import time import urllib.request from io import BytesIO, UnsupportedOperation from pathlib import Path @@ -12,7 +13,7 @@ from fs import errors, open_fs import cumulusci -from cumulusci.utils import fileutils, temporary_dir +from cumulusci.utils import fileutils, temporary_dir, update_tree from cumulusci.utils.fileutils import ( FSResource, load_from_source, @@ -253,3 +254,31 @@ class TestFSResourceError: def test_fs_resource_init_error(self): with pytest.raises(NotImplementedError): FSResource() + + +def test_update_tree(tmpdir): + source_dir = Path(tmpdir.mkdir("source")) + source_file = source_dir / "testfile.txt" + source_file.write_text("original content") + + dest_dir = Path(tmpdir.mkdir("dest")) + dest_file = dest_dir / "testfile.txt" + dest_file.write_text("modified content") + + # Ensure the source file has an older timestamp + past_time = time.time() - 100 + os.utime(str(source_file), (past_time, past_time)) + + update_tree(source_dir, dest_dir) + + assert dest_file.read_text() == "modified content" + + # Add a new file to source and run update_tree again + new_source_file = source_dir / "newfile.txt" + new_source_file.write_text("new file content") + update_tree(source_dir, dest_dir) + + # Verify that the new file is copied to destination + new_dest_file = dest_dir / "newfile.txt" + assert new_dest_file.exists() + assert new_dest_file.read_text() == "new file content" From 33bb24197ce988d01d53351dbbd4d994596b6840 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:14:40 -0800 Subject: [PATCH 84/98] Update default API version to v59.0 (#3680) This PR updates the default Salesforce API version to 59.0 and refactors various tasks and tests to use the global default API version. - Refactored CreatePackageVersion in create_package_version.py to use dynamic API versioning. Fixes #3709 - Updated tasks that had previously been manually updated each release (`github_package_data` and `activate_flow`) for dynamic API versioning. - Revised tests across several files to use CURRENT_SF_API_VERSION from cumulusci/tests/util.py. --------- Co-authored-by: github-actions Co-authored-by: James Estevez --- cumulusci/cli/tests/test_org.py | 33 +++++--- cumulusci/core/config/tests/test_config.py | 78 ++++++++++++------- cumulusci/cumulusci.yml | 3 +- .../salesforce_api/tests/test_rest_deploy.py | 19 +++-- cumulusci/tasks/bulkdata/tests/test_upsert.py | 2 +- cumulusci/tasks/create_package_version.py | 6 +- cumulusci/tasks/github/commit_status.py | 3 +- .../tasks/github/tests/test_commit_status.py | 6 +- cumulusci/tasks/salesforce/activate_flow.py | 2 +- .../salesforce/tests/test_activate_flow.py | 31 ++------ .../tests/test_create_package_version.py | 13 +++- cumulusci/tests/util.py | 3 +- 12 files changed, 115 insertions(+), 84 deletions(-) diff --git a/cumulusci/cli/tests/test_org.py b/cumulusci/cli/tests/test_org.py index c6ed45ed80..dfa7874c9e 100644 --- a/cumulusci/cli/tests/test_org.py +++ b/cumulusci/cli/tests/test_org.py @@ -23,6 +23,7 @@ ) from cumulusci.core.keychain import BaseProjectKeychain from cumulusci.core.tests.utils import MockLookup +from cumulusci.tests.util import CURRENT_SF_API_VERSION from cumulusci.utils import parse_api_datetime from .. import org @@ -108,7 +109,7 @@ def test_org_connect(self, auth_code_flow): ) responses.add( method="GET", - url="https://instance/services/data/v45.0/sobjects/Organization/OODxxxxxxxxxxxx", + url=f"https://instance/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Organization/OODxxxxxxxxxxxx", json={ "TrialExpirationDate": None, "OrganizationType": "Developer Edition", @@ -118,7 +119,11 @@ def test_org_connect(self, auth_code_flow): }, status=200, ) - responses.add("GET", "https://instance/services/data", json=[{"version": 45.0}]) + responses.add( + "GET", + "https://instance/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], + ) result = run_cli_command("org", "connect", "test", "--default", runtime=runtime) @@ -161,7 +166,7 @@ def test_org_connect__non_default_connected_app(self, auth_code_flow): ) responses.add( method="GET", - url="https://instance/services/data/v45.0/sobjects/Organization/OODxxxxxxxxxxxx", + url=f"https://instance/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Organization/OODxxxxxxxxxxxx", json={ "TrialExpirationDate": None, "OrganizationType": "Developer Edition", @@ -171,7 +176,11 @@ def test_org_connect__non_default_connected_app(self, auth_code_flow): }, status=200, ) - responses.add("GET", "https://instance/services/data", json=[{"version": 45.0}]) + responses.add( + "GET", + "https://instance/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], + ) result = run_cli_command( "org", "connect", "test", "--connected_app", "other", runtime=runtime @@ -288,7 +297,7 @@ def test_org_connect_expires(self, oauth2client): ) responses.add( method="GET", - url="https://instance/services/data/v45.0/sobjects/Organization/OODxxxxxxxxxxxx", + url=f"https://instance/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Organization/OODxxxxxxxxxxxx", json={ "TrialExpirationDate": "1970-01-01T12:34:56.000+0000", "OrganizationType": "Developer Edition", @@ -298,7 +307,11 @@ def test_org_connect_expires(self, oauth2client): }, status=200, ) - responses.add("GET", "https://instance/services/data", json=[{"version": 45.0}]) + responses.add( + "GET", + "https://instance/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], + ) run_click_command( org.org_connect, @@ -415,11 +428,11 @@ def test_org_import__persistent_org(self, cmd): method="GET", url="https://instance/services/data", status=200, - json=[{"version": "54.0"}], + json=[{"version": CURRENT_SF_API_VERSION}], ) responses.add( method="GET", - url="https://instance/services/data/v54.0/sobjects/Organization/OODxxxxxxxxxxxx", + url=f"https://instance/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Organization/OODxxxxxxxxxxxx", json={ "TrialExpirationDate": None, "OrganizationType": "Developer Edition", @@ -470,11 +483,11 @@ def test_org_import__trial_org(self, cmd): method="GET", url="https://instance/services/data", status=200, - json=[{"version": "54.0"}], + json=[{"version": CURRENT_SF_API_VERSION}], ) responses.add( method="GET", - url="https://instance/services/data/v54.0/sobjects/Organization/OODxxxxxxxxxxxx", + url=f"https://instance/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Organization/OODxxxxxxxxxxxx", json={ "TrialExpirationDate": api_datetime, "OrganizationType": "Developer Edition", diff --git a/cumulusci/core/config/tests/test_config.py b/cumulusci/core/config/tests/test_config.py index bd3e2bed1d..9656964daa 100644 --- a/cumulusci/core/config/tests/test_config.py +++ b/cumulusci/core/config/tests/test_config.py @@ -41,7 +41,7 @@ BaseProjectKeychain, ) from cumulusci.core.source import LocalFolderSource -from cumulusci.tests.util import DummyKeychain +from cumulusci.tests.util import CURRENT_SF_API_VERSION, DummyKeychain from cumulusci.utils import temporary_dir, touch from cumulusci.utils.version_strings import StrictVersion from cumulusci.utils.yaml.cumulusci_yml import GitHubSourceModel, LocalFolderSourceModel @@ -1038,11 +1038,13 @@ def test_lightning_base_url__mydomain(self): @responses.activate def test_get_salesforce_version(self): responses.add( - "GET", "https://na01.salesforce.com/services/data", json=[{"version": 42.0}] + "GET", + "https://na01.salesforce.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) config = OrgConfig({"instance_url": "https://na01.salesforce.com"}, "test") config.access_token = "TOKEN" - assert config.latest_api_version == "42.0" + assert config.latest_api_version == CURRENT_SF_API_VERSION @responses.activate def test_get_salesforce_version_bad_json(self): @@ -1093,12 +1095,14 @@ def test_load_orginfo(self): "test", ) responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/Organization/OODxxxxxxxxxxxx", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Organization/OODxxxxxxxxxxxx", json={ "OrganizationType": "Enterprise Edition", "IsSandbox": False, @@ -1129,11 +1133,15 @@ def test_get_community_info__fetch_if_not_in_cache(self): The cache should be refreshed automatically if the requested community is not in the cache. """ - responses.add("GET", "https://test/services/data", json=[{"version": 48.0}]) + responses.add( + "GET", + "https://test/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], + ) responses.add( "GET", - "https://test/services/data/v48.0/connect/communities", + f"https://test/services/data/v{CURRENT_SF_API_VERSION}/connect/communities", json={"communities": [{"name": "Kōkua"}]}, ) @@ -1387,12 +1395,14 @@ def test_is_person_accounts_enabled__not_enabled(self): ), "_is_person_accounts_enabled should be initialized as None" responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/Account/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Account/describe", json={"fields": [{"name": "Id"}]}, ) @@ -1422,12 +1432,14 @@ def test_is_person_accounts_enabled__is_enabled(self): ), "_is_person_accounts_enabled should be initialized as None" responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/Account/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Account/describe", json={"fields": [{"name": "Id"}, {"name": "IsPersonAccount"}]}, ) @@ -1458,7 +1470,9 @@ def test_is_multi_currency_enabled__not_enabled(self): # Login call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # CurrencyType describe() call. @@ -1466,7 +1480,7 @@ def test_is_multi_currency_enabled__not_enabled(self): # Therefore, the describe call will result in a 404. responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/CurrencyType/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/CurrencyType/describe", status=404, json={ "errorCode": "NOT_FOUND", @@ -1477,7 +1491,7 @@ def test_is_multi_currency_enabled__not_enabled(self): # Add a second 404 to demonstrate we always check the describe until we detect Multiple Currencies is enabled. From then on, we cache the fact that Multiple Currencies is enabled knowing Multiple Currencies cannot be disabled. responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/CurrencyType/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/CurrencyType/describe", status=404, json={ "errorCode": "NOT_FOUND", @@ -1523,14 +1537,16 @@ def test_is_multi_currency_enabled__is_enabled(self): # Token call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # CurrencyType describe() call. # Since Multiple Currencies is enabled, so the describe call returns a 200. responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/CurrencyType/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/CurrencyType/describe", json={ # The actual payload doesn't matter; only matters is we get a 200. }, @@ -1572,7 +1588,9 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_not_enable # Token call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # DatedConversionRate describe() call. @@ -1580,7 +1598,7 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_not_enable # Therefore, the describe call will result in a 404. responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/DatedConversionRate/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/DatedConversionRate/describe", status=404, json={ "errorCode": "NOT_FOUND", @@ -1613,7 +1631,9 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_enabled__a # Token call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # DatedConversionRate describe() call. @@ -1621,7 +1641,7 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_enabled__a # However, ACM is not enabled so DatedConversionRate is not createable. responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/DatedConversionRate/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/DatedConversionRate/describe", json={"createable": False}, ) @@ -1651,7 +1671,9 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_enabled__a # Token call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # DatedConversionRate describe() call. @@ -1659,7 +1681,7 @@ def test_is_advanced_currency_management_enabled__multiple_currencies_enabled__a # However, ACM is not enabled so DatedConversionRate is not createable. responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/DatedConversionRate/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/DatedConversionRate/describe", json={"createable": True}, ) @@ -1687,13 +1709,15 @@ def test_is_survey_advanced_features_enabled(self): # Token call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # describe() responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/PermissionSet/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/PermissionSet/describe", json={"fields": [{"name": "PermissionsAllowSurveyAdvancedFeatures"}]}, ) @@ -1712,13 +1736,15 @@ def test_is_survey_advanced_features_enabled__not_enabled(self): # Token call. responses.add( - "GET", "https://example.com/services/data", json=[{"version": 48.0}] + "GET", + "https://example.com/services/data", + json=[{"version": CURRENT_SF_API_VERSION}], ) # describe() responses.add( "GET", - "https://example.com/services/data/v48.0/sobjects/PermissionSet/describe", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/PermissionSet/describe", json={"fields": [{"name": "foo"}]}, ) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 87f75f38b6..bcdd32f69e 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -7,7 +7,6 @@ tasks: class_path: cumulusci.tasks.salesforce.activate_flow.ActivateFlow options: status: True - deactivate_flow: group: Metadata Transformations description: deactivates Flows identified by a given list of Developer Names @@ -1466,7 +1465,7 @@ project: namespace: install_class: uninstall_class: - api_version: "55.0" + api_version: "59.0" git: default_branch: master prefix_feature: feature/ diff --git a/cumulusci/salesforce_api/tests/test_rest_deploy.py b/cumulusci/salesforce_api/tests/test_rest_deploy.py index 6a3794e35a..cee9dd941d 100644 --- a/cumulusci/salesforce_api/tests/test_rest_deploy.py +++ b/cumulusci/salesforce_api/tests/test_rest_deploy.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, Mock, call, patch from cumulusci.salesforce_api.rest_deploy import RestDeploy +from cumulusci.tests.util import CURRENT_SF_API_VERSION def generate_sample_zip_data(parent=""): @@ -28,7 +29,9 @@ def setUp(self): self.mock_task.logger = self.mock_logger self.mock_task.org_config.instance_url = "https://example.com" self.mock_task.org_config.access_token = "dummy_token" - self.mock_task.project_config.project__package__api_version = 58.0 + self.mock_task.project_config.project__package__api_version = ( + CURRENT_SF_API_VERSION + ) # Empty zip file for testing self.mock_zip = generate_sample_zip_data() @@ -64,11 +67,11 @@ def test_deployment_success(self, mock_get, mock_post): # Assertions to verify API Calls expected_get_calls = [ call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), ] @@ -160,11 +163,11 @@ def test_deployStatus_failure(self, mock_get, mock_post): # Assertions to verify API Calls expected_get_calls = [ call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), ] @@ -206,15 +209,15 @@ def test_pending_call(self, mock_get, mock_post): # Assertions to verify API Calls expected_get_calls = [ call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), call( - "https://example.com/services/data/v58.0/metadata/deployRequest/dummy_id?includeDetails=true", + f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/metadata/deployRequest/dummy_id?includeDetails=true", headers={"Authorization": "Bearer dummy_token"}, ), ] diff --git a/cumulusci/tasks/bulkdata/tests/test_upsert.py b/cumulusci/tasks/bulkdata/tests/test_upsert.py index 370cd6488a..8caa7ecb55 100644 --- a/cumulusci/tasks/bulkdata/tests/test_upsert.py +++ b/cumulusci/tasks/bulkdata/tests/test_upsert.py @@ -239,7 +239,7 @@ def test_upsert_rest__faked( def _mock_bulk(self, domain): responses.add( method="GET", - url=f"https://{domain}/services/data/v52.0/limits/recordCount?sObjects=Contact", + url=f"https://{domain}/services/data/v{CURRENT_SF_API_VERSION}/limits/recordCount?sObjects=Contact", status=200, json={"sObjects": []}, ) diff --git a/cumulusci/tasks/create_package_version.py b/cumulusci/tasks/create_package_version.py index b4d5a264a4..4104e2c0f7 100644 --- a/cumulusci/tasks/create_package_version.py +++ b/cumulusci/tasks/create_package_version.py @@ -97,8 +97,6 @@ class CreatePackageVersion(BaseSalesforceApiTask): org is a scratch org with the correct configuration for these purposes. """ - api_version = "52.0" - task_options = { "package_name": {"description": "Name of package"}, "package_type": { @@ -201,7 +199,7 @@ def _init_task(self): self.tooling = get_simple_salesforce_connection( self.project_config, get_devhub_config(self.project_config), - api_version=self.api_version, + api_version=self.project_config.project__package__api_version, base_url="tooling", ) self.context = TaskContext(self.org_config, self.project_config, self.logger) @@ -424,7 +422,7 @@ def _create_version_request( with build_settings_package( scratch_org_def.get("settings"), scratch_org_def.get("objectSettings"), - self.api_version, + self.project_config.project__package__api_version, ) as path: settings_zip_builder = MetadataPackageZipBuilder( path=path, context=self.context diff --git a/cumulusci/tasks/github/commit_status.py b/cumulusci/tasks/github/commit_status.py index 7793fd8027..a6edc14ceb 100644 --- a/cumulusci/tasks/github/commit_status.py +++ b/cumulusci/tasks/github/commit_status.py @@ -5,8 +5,6 @@ class GetPackageDataFromCommitStatus(BaseGithubTask, BaseSalesforceApiTask): - api_version = "52.0" - task_options = { "context": { "description": "Name of the commit status context", @@ -16,6 +14,7 @@ class GetPackageDataFromCommitStatus(BaseGithubTask, BaseSalesforceApiTask): } def _run_task(self): + self.api_version = self.project_config.project__api_version repo = self.get_repo() context = self.options["context"] commit_sha = self.project_config.repo_commit diff --git a/cumulusci/tasks/github/tests/test_commit_status.py b/cumulusci/tasks/github/tests/test_commit_status.py index a1d80388fa..04d1ee39e0 100644 --- a/cumulusci/tasks/github/tests/test_commit_status.py +++ b/cumulusci/tasks/github/tests/test_commit_status.py @@ -7,7 +7,7 @@ from cumulusci.core.exceptions import DependencyLookupError from cumulusci.tasks.github.commit_status import GetPackageDataFromCommitStatus from cumulusci.tasks.github.tests.util_github_api import GithubApiTestMixin -from cumulusci.tests.util import create_project_config +from cumulusci.tests.util import CURRENT_SF_API_VERSION, create_project_config class TestGetPackageDataFromCommitStatus(GithubApiTestMixin): @@ -48,7 +48,7 @@ def test_run_task(self): ) responses.add( "GET", - "https://salesforce/services/data/v52.0/tooling/query/", + f"https://salesforce/services/data/v{CURRENT_SF_API_VERSION}/tooling/query/", json={ "records": [ {"Dependencies": {"ids": [{"subscriberPackageVersionId": "04t_2"}]}} @@ -168,7 +168,7 @@ def test_run_task__status_not_found(self): def test_get_dependencies__version_not_found(self): responses.add( "GET", - "https://salesforce/services/data/v52.0/tooling/query/", + f"https://salesforce/services/data/v{CURRENT_SF_API_VERSION}/tooling/query/", json={"records": []}, ) diff --git a/cumulusci/tasks/salesforce/activate_flow.py b/cumulusci/tasks/salesforce/activate_flow.py index 9bbdbc2c64..208ab62ea6 100644 --- a/cumulusci/tasks/salesforce/activate_flow.py +++ b/cumulusci/tasks/salesforce/activate_flow.py @@ -24,7 +24,7 @@ def _init_options(self, kwargs): self.options["developer_names"] = process_list_arg( self.options.get("developer_names") ) - self.api_version = "58.0" + self.api_version = self.project_config.project__api_version if not self.options["developer_names"]: raise TaskOptionsError( "Error you are missing developer_names definition in your task cumulusci.yml file. Please pass in developer_names for your task configuration or use -o to developer_names as a commandline argument" diff --git a/cumulusci/tasks/salesforce/tests/test_activate_flow.py b/cumulusci/tasks/salesforce/tests/test_activate_flow.py index c89a7c2a2f..c771d3f0b7 100644 --- a/cumulusci/tasks/salesforce/tests/test_activate_flow.py +++ b/cumulusci/tasks/salesforce/tests/test_activate_flow.py @@ -5,6 +5,7 @@ from cumulusci.core.exceptions import TaskOptionsError from cumulusci.tasks.salesforce.activate_flow import ActivateFlow +from cumulusci.tests.util import CURRENT_SF_API_VERSION from .util import create_task @@ -23,14 +24,10 @@ def test_activate_some_flow_processes(self): }, ) record_id = "3001F0000009GFwQAM" - activate_url = ( - "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( - cc_task.org_config.instance_url, record_id - ) - ) + activate_url = f"{cc_task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/tooling/sobjects/FlowDefinition/{record_id}" responses.add( method="GET", - url="https://test.salesforce.com/services/data/v58.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", + url=f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", body=json.dumps( { "records": [ @@ -63,14 +60,10 @@ def test_deactivate_some_flow_processes(self): }, ) record_id = "3001F0000009GFwQAM" - activate_url = ( - "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( - cc_task.org_config.instance_url, record_id - ) - ) + activate_url = f"{cc_task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/tooling/sobjects/FlowDefinition/{record_id}" responses.add( method="GET", - url="https://test.salesforce.com/services/data/v58.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", + url=f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", body=json.dumps( { "records": [ @@ -104,19 +97,11 @@ def test_activate_all_flow_processes(self): ) record_id = "3001F0000009GFwQAM" record_id2 = "3001F0000009GFwQAW" - activate_url = ( - "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( - cc_task.org_config.instance_url, record_id - ) - ) - activate_url2 = ( - "{}/services/data/v58.0/tooling/sobjects/FlowDefinition/{}".format( - cc_task.org_config.instance_url, record_id2 - ) - ) + activate_url = f"{cc_task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/tooling/sobjects/FlowDefinition/{record_id}" + activate_url2 = f"{cc_task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/tooling/sobjects/FlowDefinition/{record_id2}" responses.add( method="GET", - url="https://test.salesforce.com/services/data/v58.0/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", + url=f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling/query/?q=SELECT+Id%2C+ActiveVersion.VersionNumber%2C+LatestVersion.VersionNumber%2C+DeveloperName+FROM+FlowDefinition+WHERE+DeveloperName+IN+%28%27Auto_Populate_Date_And_Name_On_Program_Engagement%27%2C%27ape%27%29", body=json.dumps( { "records": [ diff --git a/cumulusci/tasks/tests/test_create_package_version.py b/cumulusci/tasks/tests/test_create_package_version.py index 32057828f3..6d096012b5 100644 --- a/cumulusci/tasks/tests/test_create_package_version.py +++ b/cumulusci/tasks/tests/test_create_package_version.py @@ -34,8 +34,11 @@ PackageTypeEnum, VersionTypeEnum, ) +from cumulusci.tests.util import CURRENT_SF_API_VERSION from cumulusci.utils import temporary_dir, touch +print(CURRENT_SF_API_VERSION) + @pytest.fixture def repo_root(): @@ -180,8 +183,12 @@ def test_validate_uninstall_script(self): class TestCreatePackageVersion: - devhub_base_url = "https://devhub.my.salesforce.com/services/data/v52.0" - scratch_base_url = "https://scratch.my.salesforce.com/services/data/v52.0" + devhub_base_url = ( + f"https://devhub.my.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}" + ) + scratch_base_url = ( + f"https://scratch.my.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}" + ) def test_postinstall_script_logic(self, get_task): task = get_task({"package_type": "Managed", "package_name": "Foo"}) @@ -258,7 +265,7 @@ def test_run_task( responses.add( # get dependency org API version "GET", "https://scratch.my.salesforce.com/services/data", - json=[{"version": "52.0"}], + json=[{"version": CURRENT_SF_API_VERSION}], ) responses.add( # query for dependency org installed packages "GET", diff --git a/cumulusci/tests/util.py b/cumulusci/tests/util.py index a9ed45215b..ad9bd6620f 100644 --- a/cumulusci/tests/util.py +++ b/cumulusci/tests/util.py @@ -23,7 +23,8 @@ ) from cumulusci.core.keychain import BaseProjectKeychain -CURRENT_SF_API_VERSION = "55.0" +# putting this below FakeBulkAPI causes a circular import +CURRENT_SF_API_VERSION = UniversalConfig().project__package__api_version from cumulusci.tasks.bulkdata.tests.utils import FakeBulkAPI From 1e9d6fd361f41444878e14a8eae717516469d301 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Fri, 8 Dec 2023 11:30:05 +0530 Subject: [PATCH 85/98] draft --- cumulusci/cumulusci.yml | 4 + cumulusci/tasks/preflight/checks.py | 134 ++++++++++++++++++ .../tasks/salesforce/DescribeMetadataTypes.py | 4 +- 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 cumulusci/tasks/preflight/checks.py diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 87f75f38b6..3894daebf4 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -1,6 +1,10 @@ cumulusci: keychain: cumulusci.core.keychain.EncryptedFileProjectKeychain tasks: + get_preflight_checks: + class_path: cumulusci.tasks.preflight.checks.RetrievePreflightChecks + group: Salesforce Preflight Checks + activate_flow: group: Metadata Transformations description: Activates Flows identified by a given list of Developer Names diff --git a/cumulusci/tasks/preflight/checks.py b/cumulusci/tasks/preflight/checks.py new file mode 100644 index 0000000000..2c316db6b9 --- /dev/null +++ b/cumulusci/tasks/preflight/checks.py @@ -0,0 +1,134 @@ +from cumulusci.core.config import TaskConfig +from cumulusci.tasks.preflight.licenses import ( + GetAvailableLicenses, + GetAvailablePermissionSetLicenses, + GetAvailablePermissionSets, + GetPermissionLicenseSetAssignments, +) +from cumulusci.tasks.preflight.packages import GetInstalledPackages +from cumulusci.tasks.preflight.permsets import GetPermissionSetAssignments +from cumulusci.tasks.preflight.recordtypes import CheckSObjectRecordTypes +from cumulusci.tasks.preflight.settings import CheckMyDomainActive, CheckSettingsValue +from cumulusci.tasks.preflight.sobjects import ( + CheckSObjectOWDs, + CheckSObjectPerms, + CheckSObjectsAvailable, +) +from cumulusci.tasks.salesforce import BaseSalesforceApiTask, DescribeMetadataTypes + + +class RetrievePreflightChecks(BaseSalesforceApiTask): + task_options = { + "object_permissions": { + "description": "The object permissions to check. Each key should be an sObject API name, whose value is a map of describe keys, " + "such as `queryable` and `createable`, to their desired values (True or False). The output is True if all sObjects and permissions " + "are present and matching the specification. See the task documentation for examples." + }, + "settings_type": { + "description": "The API name of the Settings entity to be checked, such as ChatterSettings.", + }, + "settings_field": { + "description": "The API name of the field on the Settings entity to check.", + }, + "settings_value": {"description": "The value to check for the settings entity"}, + "treat_missing_setting_as_failure": { + "description": "If True, treat a missing Settings entity as a preflight failure, instead of raising an exception. Defaults to False.", + }, + "object_org_wide_defaults": { + "description": "The Organization-Wide Defaults to check, " + "organized as a list with each element containing the keys api_name, " + "internal_sharing_model, and external_sharing_model. NOTE: you must have " + "External Sharing Model turned on in Sharing Settings to use the latter feature. " + "Checking External Sharing Model when it is turned off will fail the preflight.", + }, + } + + def _init_task(self): + super()._init_task() + + def _run_task(self): + + classes = [ + CheckMyDomainActive, + GetAvailableLicenses, + GetAvailablePermissionSetLicenses, + GetPermissionLicenseSetAssignments, + GetAvailablePermissionSets, + GetInstalledPackages, + GetPermissionSetAssignments, + CheckSObjectRecordTypes, + CheckSObjectsAvailable, + DescribeMetadataTypes, + ] + + self.return_values = { + cls.__name__: cls( + org_config=self.org_config, + project_config=self.project_config, + task_config=self.task_config, + )() + for cls in classes + } + + if "object_permissions" in self.options: + task_config = TaskConfig( + {"options": {"permissions": self.options["object_permissions"]}} + ) + try: + self.return_values["CheckSObjectPerms"] = CheckSObjectPerms( + org_config=self.org_config, + project_config=self.project_config, + task_config=task_config, + )() + except Exception as e: + self.logger.error("[Error]CheckSObjectPerms: " + str(e)) + else: + self.logger.info( + "There were no specified SobjectPermissions for validation." + ) + + if "object_org_wide_defaults" in self.options: + task_config = task_config = TaskConfig( + { + "options": { + "org_wide_defaults": self.options["object_org_wide_defaults"] + } + } + ) + + try: + self.return_values["CheckSObjectOWDs"] = CheckSObjectOWDs( + org_config=self.org_config, + project_config=self.project_config, + task_config=task_config, + ) + except Exception as e: + self.logger.error("[Error]:CheckSObjectOWDs " + str(e)) + + if "settings_type" and "settings_field" and "settings_value" in self.options: + if "treat_missing_setting_as_failure" not in self.options: + self.options["treat_missing_setting_as_failure"] = False + task_config = task_config = TaskConfig( + { + "options": { + "settings_type": self.options["settings_type"], + "settings_field": self.options["settings_field"], + "value": self.options["settings_value"], + "treat_missing_setting_as_failure": self.options[ + "missing_settingentity_as_failure" + ], + } + } + ) + try: + self.return_values["CheckSettingsValue"] = CheckSettingsValue( + org_config=self.org_config, + project_config=self.project_config, + task_config=task_config, + )() + except Exception as e: + self.logger.error("[Error]CheckSettingsValue: " + str(e)) + else: + self.logger.info( + "Checking the value for settings requires information on the type, value, and field." + ) diff --git a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py index 49c605c37d..0b2d4f4e1e 100644 --- a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py +++ b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py @@ -22,5 +22,5 @@ def _get_api(self): def _run_task(self): api_object = self._get_api() - metadata_list = api_object() - self.logger.info("Metadata Types supported by org:\n" + str(metadata_list)) + self.return_values = api_object() + self.logger.info("Metadata Types supported by org:\n" + str(self.return_values)) From 359425d28b60656909875bbf07a4b23b8ed6e7f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:03:41 -0800 Subject: [PATCH 86/98] Release v3.83.0 (#3716) Co-authored-by: github-actions Co-authored-by: James Estevez --- cumulusci/__about__.py | 2 +- docs/history.md | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cumulusci/__about__.py b/cumulusci/__about__.py index 56b944aaa3..30420af940 100644 --- a/cumulusci/__about__.py +++ b/cumulusci/__about__.py @@ -1 +1 @@ -__version__ = "3.82.0" +__version__ = "3.83.0" diff --git a/docs/history.md b/docs/history.md index 16204b4b92..48b763e338 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,22 @@ +## v3.83.0 (2023-12-08) + + + +## What's Changed + +### Changes 🎉 + +- Allowed namespace injection without managed by [@jain-naman-sf](https://github.com/jain-naman-sf) in [#3677](https://github.com/SFDO-Tooling/CumulusCI/pull/3677) +- Update pyright configuration for 1.1.339 by [@jstvz](https://github.com/jstvz) in [#3715](https://github.com/SFDO-Tooling/CumulusCI/pull/3715) +- Replace distutils with shutil for 3.12 by [@jstvz](https://github.com/jstvz) in [#3714](https://github.com/SFDO-Tooling/CumulusCI/pull/3714) + +**Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.82.0...v3.83.0 + + + ## v3.82.0 (2023-12-01) @@ -30,8 +46,6 @@ **Full Changelog**: https://github.com/SFDO-Tooling/CumulusCI/compare/v3.81.0...v3.82.0 - - ## v3.81.0 (2023-11-03) From 7de7b544266113dbc0b177465b46d91a2da934f6 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Mon, 18 Dec 2023 18:03:48 +0530 Subject: [PATCH 87/98] added_task --- cumulusci/cumulusci.yml | 6 ++-- cumulusci/tasks/preflight/retrieve_tasks.py | 28 +++++++++++++++ .../preflight/tests/test_retrieve_tasks.py | 35 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 cumulusci/tasks/preflight/retrieve_tasks.py create mode 100644 cumulusci/tasks/preflight/tests/test_retrieve_tasks.py diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 3894daebf4..ad4985fc72 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -1,9 +1,9 @@ cumulusci: keychain: cumulusci.core.keychain.EncryptedFileProjectKeychain tasks: - get_preflight_checks: - class_path: cumulusci.tasks.preflight.checks.RetrievePreflightChecks - group: Salesforce Preflight Checks + retrieve_tasks: + class_path: cumulusci.tasks.retrieve_tasks.RetrieveTasks + description: Retrieves the tasks under the particular category or group activate_flow: group: Metadata Transformations diff --git a/cumulusci/tasks/preflight/retrieve_tasks.py b/cumulusci/tasks/preflight/retrieve_tasks.py new file mode 100644 index 0000000000..4fac12ab06 --- /dev/null +++ b/cumulusci/tasks/preflight/retrieve_tasks.py @@ -0,0 +1,28 @@ +from cumulusci.cli.runtime import CliRuntime +from cumulusci.cli.utils import group_items +from cumulusci.core.tasks import BaseTask + + +class RetrieveTasks(BaseTask): + task_options = { + "group_name": { + "description": "Tasks under the category you wish to list", + "required": True, + }, + } + + def _run_task(self): + runtime = CliRuntime(load_keychain=True) + tasks = runtime.get_available_tasks() + + task_groups = group_items(tasks) + + task_groups = task_groups[self.options["group_name"]] + self.return_values = [] + for task_name, description in task_groups: + self.return_values.append(task_name) + + if self.return_values: + self.return_values.sort() + + self.logger.info(self.return_values) diff --git a/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py b/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py new file mode 100644 index 0000000000..5cf159b9ce --- /dev/null +++ b/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py @@ -0,0 +1,35 @@ +from cumulusci.tasks.preflight.retrieve_tasks import RetrieveTasks +from cumulusci.tasks.salesforce.tests.util import create_task + + +class TestRetrieveTasks: + def test_run_task(self): + expected_output = [ + "check_advanced_currency_management", + "check_chatter_enabled", + "check_enhanced_notes_enabled", + "check_my_domain_active", + "check_org_settings_value", + "check_org_wide_defaults", + "check_sobject_permissions", + "check_sobjects_available", + "get_assigned_permission_set_licenses", + "get_assigned_permission_sets", + "get_available_licenses", + "get_available_permission_set_licenses", + "get_available_permission_sets", + "get_existing_record_types", + "get_existing_sites", + "get_installed_packages", + ] + task = create_task( + RetrieveTasks, options={"group_name": "Salesforce Preflight Checks"} + ) + output = task() + assert output == expected_output + + def test_run_nogroup_name(self): + expected_output = [] + task = create_task(RetrieveTasks, options={"group_name": "Temperorry checks"}) + output = task() + assert output == expected_output From 5ac010bdaaac2d8b0a28fbb399ebcf3f3342b5cb Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Mon, 18 Dec 2023 18:07:48 +0530 Subject: [PATCH 88/98] added_task --- cumulusci/tasks/preflight/checks.py | 134 ---------------------------- 1 file changed, 134 deletions(-) delete mode 100644 cumulusci/tasks/preflight/checks.py diff --git a/cumulusci/tasks/preflight/checks.py b/cumulusci/tasks/preflight/checks.py deleted file mode 100644 index 2c316db6b9..0000000000 --- a/cumulusci/tasks/preflight/checks.py +++ /dev/null @@ -1,134 +0,0 @@ -from cumulusci.core.config import TaskConfig -from cumulusci.tasks.preflight.licenses import ( - GetAvailableLicenses, - GetAvailablePermissionSetLicenses, - GetAvailablePermissionSets, - GetPermissionLicenseSetAssignments, -) -from cumulusci.tasks.preflight.packages import GetInstalledPackages -from cumulusci.tasks.preflight.permsets import GetPermissionSetAssignments -from cumulusci.tasks.preflight.recordtypes import CheckSObjectRecordTypes -from cumulusci.tasks.preflight.settings import CheckMyDomainActive, CheckSettingsValue -from cumulusci.tasks.preflight.sobjects import ( - CheckSObjectOWDs, - CheckSObjectPerms, - CheckSObjectsAvailable, -) -from cumulusci.tasks.salesforce import BaseSalesforceApiTask, DescribeMetadataTypes - - -class RetrievePreflightChecks(BaseSalesforceApiTask): - task_options = { - "object_permissions": { - "description": "The object permissions to check. Each key should be an sObject API name, whose value is a map of describe keys, " - "such as `queryable` and `createable`, to their desired values (True or False). The output is True if all sObjects and permissions " - "are present and matching the specification. See the task documentation for examples." - }, - "settings_type": { - "description": "The API name of the Settings entity to be checked, such as ChatterSettings.", - }, - "settings_field": { - "description": "The API name of the field on the Settings entity to check.", - }, - "settings_value": {"description": "The value to check for the settings entity"}, - "treat_missing_setting_as_failure": { - "description": "If True, treat a missing Settings entity as a preflight failure, instead of raising an exception. Defaults to False.", - }, - "object_org_wide_defaults": { - "description": "The Organization-Wide Defaults to check, " - "organized as a list with each element containing the keys api_name, " - "internal_sharing_model, and external_sharing_model. NOTE: you must have " - "External Sharing Model turned on in Sharing Settings to use the latter feature. " - "Checking External Sharing Model when it is turned off will fail the preflight.", - }, - } - - def _init_task(self): - super()._init_task() - - def _run_task(self): - - classes = [ - CheckMyDomainActive, - GetAvailableLicenses, - GetAvailablePermissionSetLicenses, - GetPermissionLicenseSetAssignments, - GetAvailablePermissionSets, - GetInstalledPackages, - GetPermissionSetAssignments, - CheckSObjectRecordTypes, - CheckSObjectsAvailable, - DescribeMetadataTypes, - ] - - self.return_values = { - cls.__name__: cls( - org_config=self.org_config, - project_config=self.project_config, - task_config=self.task_config, - )() - for cls in classes - } - - if "object_permissions" in self.options: - task_config = TaskConfig( - {"options": {"permissions": self.options["object_permissions"]}} - ) - try: - self.return_values["CheckSObjectPerms"] = CheckSObjectPerms( - org_config=self.org_config, - project_config=self.project_config, - task_config=task_config, - )() - except Exception as e: - self.logger.error("[Error]CheckSObjectPerms: " + str(e)) - else: - self.logger.info( - "There were no specified SobjectPermissions for validation." - ) - - if "object_org_wide_defaults" in self.options: - task_config = task_config = TaskConfig( - { - "options": { - "org_wide_defaults": self.options["object_org_wide_defaults"] - } - } - ) - - try: - self.return_values["CheckSObjectOWDs"] = CheckSObjectOWDs( - org_config=self.org_config, - project_config=self.project_config, - task_config=task_config, - ) - except Exception as e: - self.logger.error("[Error]:CheckSObjectOWDs " + str(e)) - - if "settings_type" and "settings_field" and "settings_value" in self.options: - if "treat_missing_setting_as_failure" not in self.options: - self.options["treat_missing_setting_as_failure"] = False - task_config = task_config = TaskConfig( - { - "options": { - "settings_type": self.options["settings_type"], - "settings_field": self.options["settings_field"], - "value": self.options["settings_value"], - "treat_missing_setting_as_failure": self.options[ - "missing_settingentity_as_failure" - ], - } - } - ) - try: - self.return_values["CheckSettingsValue"] = CheckSettingsValue( - org_config=self.org_config, - project_config=self.project_config, - task_config=task_config, - )() - except Exception as e: - self.logger.error("[Error]CheckSettingsValue: " + str(e)) - else: - self.logger.info( - "Checking the value for settings requires information on the type, value, and field." - ) From d49a6c4e236c068c139f2fbed4eeeb8458df500a Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Mon, 18 Dec 2023 18:13:14 +0530 Subject: [PATCH 89/98] modified_cumulusci.yml --- cumulusci/cumulusci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index ad4985fc72..a72220a9a3 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -1,10 +1,6 @@ cumulusci: keychain: cumulusci.core.keychain.EncryptedFileProjectKeychain tasks: - retrieve_tasks: - class_path: cumulusci.tasks.retrieve_tasks.RetrieveTasks - description: Retrieves the tasks under the particular category or group - activate_flow: group: Metadata Transformations description: Activates Flows identified by a given list of Developer Names @@ -130,6 +126,9 @@ tasks: settings_type: EnhancedNotesSettings settings_field: IsEnhancedNotesEnabled value: True + retrieve_tasks: + class_path: cumulusci.tasks.preflight.retrieve_tasks.RetrieveTasks + description: Retrieves the tasks under the particular category or group custom_settings_value_wait: description: Waits for a specific field value on the specified custom settings object and field class_path: cumulusci.tasks.salesforce.custom_settings_wait.CustomSettingValueWait From bb82afe2b510355f13364687dc94af99e976c62e Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Tue, 19 Dec 2023 11:38:47 +0530 Subject: [PATCH 90/98] tast_cases modified --- cumulusci/tasks/preflight/retrieve_tasks.py | 8 +- .../preflight/tests/test_retrieve_tasks.py | 85 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/cumulusci/tasks/preflight/retrieve_tasks.py b/cumulusci/tasks/preflight/retrieve_tasks.py index 4fac12ab06..fc3e658f02 100644 --- a/cumulusci/tasks/preflight/retrieve_tasks.py +++ b/cumulusci/tasks/preflight/retrieve_tasks.py @@ -1,5 +1,6 @@ from cumulusci.cli.runtime import CliRuntime from cumulusci.cli.utils import group_items +from cumulusci.core.exceptions import CumulusCIException from cumulusci.core.tasks import BaseTask @@ -14,15 +15,14 @@ class RetrieveTasks(BaseTask): def _run_task(self): runtime = CliRuntime(load_keychain=True) tasks = runtime.get_available_tasks() - task_groups = group_items(tasks) - task_groups = task_groups[self.options["group_name"]] - self.return_values = [] + self.return_values = [] # type: ignore for task_name, description in task_groups: self.return_values.append(task_name) - if self.return_values: self.return_values.sort() + else: + raise CumulusCIException("No tasks in the specified group") self.logger.info(self.return_values) diff --git a/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py b/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py index 5cf159b9ce..50b739ab03 100644 --- a/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py +++ b/cumulusci/tasks/preflight/tests/test_retrieve_tasks.py @@ -1,35 +1,62 @@ +from unittest import mock + +import pytest + +from cumulusci.cli.runtime import CliRuntime +from cumulusci.core.exceptions import CumulusCIException from cumulusci.tasks.preflight.retrieve_tasks import RetrieveTasks from cumulusci.tasks.salesforce.tests.util import create_task class TestRetrieveTasks: - def test_run_task(self): - expected_output = [ - "check_advanced_currency_management", - "check_chatter_enabled", - "check_enhanced_notes_enabled", - "check_my_domain_active", - "check_org_settings_value", - "check_org_wide_defaults", - "check_sobject_permissions", - "check_sobjects_available", - "get_assigned_permission_set_licenses", - "get_assigned_permission_sets", - "get_available_licenses", - "get_available_permission_set_licenses", - "get_available_permission_sets", - "get_existing_record_types", - "get_existing_sites", - "get_installed_packages", - ] - task = create_task( - RetrieveTasks, options={"group_name": "Salesforce Preflight Checks"} - ) - output = task() - assert output == expected_output + @pytest.mark.parametrize( + "available_tasks, group_name, expected_output", + [ + ( + [ + { + "name": "test_task1", + "description": "Test Task", + "group": "Group", + }, + { + "name": "test_task2", + "description": "Test Task", + "group": "Group", + }, + { + "name": "test_task3", + "description": "Test Task", + "group": "Test Group", + }, + ], + "Group", + ["test_task1", "test_task2"], + ), + ( + [ + { + "name": "test_task1", + "description": "Test Task", + "group": "Group", + }, + ], + "Tests", + None, + ), + ], + ) + def test_run_task(self, available_tasks, group_name, expected_output): + task = create_task(RetrieveTasks, options={"group_name": group_name}) - def test_run_nogroup_name(self): - expected_output = [] - task = create_task(RetrieveTasks, options={"group_name": "Temperorry checks"}) - output = task() - assert output == expected_output + with mock.patch.object( + CliRuntime, "get_available_tasks", return_value=available_tasks + ): + if expected_output is not None: + output = task() + assert output == expected_output + else: + with pytest.raises( + CumulusCIException, match="No tasks in the specified group" + ): + task() From 5949372ca643a2cd02d3a39625227243ff57e3bc Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Tue, 19 Dec 2023 12:39:17 +0530 Subject: [PATCH 91/98] type issues resolved --- cumulusci/cumulusci.yml | 2 +- cumulusci/tasks/preflight/retrieve_tasks.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index c2d35d4912..9b13e81741 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -126,8 +126,8 @@ tasks: settings_field: IsEnhancedNotesEnabled value: True retrieve_tasks: - class_path: cumulusci.tasks.preflight.retrieve_tasks.RetrieveTasks description: Retrieves the tasks under the particular category or group + class_path: cumulusci.tasks.preflight.retrieve_tasks.RetrieveTasks custom_settings_value_wait: description: Waits for a specific field value on the specified custom settings object and field class_path: cumulusci.tasks.salesforce.custom_settings_wait.CustomSettingValueWait diff --git a/cumulusci/tasks/preflight/retrieve_tasks.py b/cumulusci/tasks/preflight/retrieve_tasks.py index fc3e658f02..b868c97e9a 100644 --- a/cumulusci/tasks/preflight/retrieve_tasks.py +++ b/cumulusci/tasks/preflight/retrieve_tasks.py @@ -1,3 +1,5 @@ +from typing import List + from cumulusci.cli.runtime import CliRuntime from cumulusci.cli.utils import group_items from cumulusci.core.exceptions import CumulusCIException @@ -7,7 +9,7 @@ class RetrieveTasks(BaseTask): task_options = { "group_name": { - "description": "Tasks under the category you wish to list", + "description": "Name of the category or Group", "required": True, }, } @@ -17,7 +19,7 @@ def _run_task(self): tasks = runtime.get_available_tasks() task_groups = group_items(tasks) task_groups = task_groups[self.options["group_name"]] - self.return_values = [] # type: ignore + self.return_values: List[str] = [] for task_name, description in task_groups: self.return_values.append(task_name) if self.return_values: From 9f88e73077c25fd21cab6b3d1f86c2560303d6ce Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Tue, 19 Dec 2023 13:46:32 +0530 Subject: [PATCH 92/98] comments added --- cumulusci/tasks/bulkdata/query_transformers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulusci/tasks/bulkdata/query_transformers.py b/cumulusci/tasks/bulkdata/query_transformers.py index d37604892d..ed78878e47 100644 --- a/cumulusci/tasks/bulkdata/query_transformers.py +++ b/cumulusci/tasks/bulkdata/query_transformers.py @@ -111,7 +111,7 @@ def outerjoins_to_add(self): ] except KeyError as e: - + # For generate_and_load_from_yaml, In case of namespace_inject true, mapping table name doesn't have namespace added try: rt_source_table = self.metadata.tables[ f"{self.mapping.table}_rt_mapping" From ae32efa9d4843e8e1dede747089e64931f0c5c13 Mon Sep 17 00:00:00 2001 From: lakshmi2506 Date: Tue, 19 Dec 2023 13:53:46 +0530 Subject: [PATCH 93/98] comments added --- cumulusci/tasks/bulkdata/query_transformers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cumulusci/tasks/bulkdata/query_transformers.py b/cumulusci/tasks/bulkdata/query_transformers.py index ed78878e47..01a240465d 100644 --- a/cumulusci/tasks/bulkdata/query_transformers.py +++ b/cumulusci/tasks/bulkdata/query_transformers.py @@ -112,6 +112,7 @@ def outerjoins_to_add(self): except KeyError as e: # For generate_and_load_from_yaml, In case of namespace_inject true, mapping table name doesn't have namespace added + # We are checking for table_rt_mapping table try: rt_source_table = self.metadata.tables[ f"{self.mapping.table}_rt_mapping" From a9ff00191d059f42af69a452cac59b94dfbe7de2 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:03:02 +0530 Subject: [PATCH 94/98] Error during upsert on identical DeveloperName for RecordType of same sObject (#3702) [W-11466074](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE000011wj7UYAQ/view) Fixed a bug during upsert (load) of records where if the sObject had two record types with the same `DeveloperName` but different `IsPersonType`, duplicate records were being generated for load and as a result, error was thrown. Example: For the sObject `Account`, the record type `PersonAccounts` has `DeveloperName = PersonAccounts` and `IsPersonType = True`. I could also create another recordType such that `DeveloperName = PersonAccounts` but the `IsPersonType = False`. In such cases, say I was trying to upsert 1 record (any update_key) with this `PersonAccounts` (one of the two), 2 records would try to get inserted and would throw a duplicate row error. --------- Co-authored-by: Jaipal Reddy Kasturi --- cumulusci/tasks/bulkdata/extract.py | 5 +- cumulusci/tasks/bulkdata/load.py | 4 +- .../tasks/bulkdata/query_transformers.py | 11 +- .../tasks/bulkdata/tests/recordtypes.sql | 7 +- .../tasks/bulkdata/tests/recordtypes_2.sql | 7 +- .../tasks/bulkdata/tests/test_extract.py | 1 + cumulusci/tasks/bulkdata/tests/test_load.py | 7 +- cumulusci/tasks/bulkdata/tests/test_upsert.py | 106 ++++++++++++++++++ cumulusci/tasks/bulkdata/tests/test_utils.py | 20 +++- cumulusci/tasks/bulkdata/utils.py | 19 ++-- ...bjects_Account_PersonAccount_describe.yaml | 18 +++ .../upsert/upsert_mapping_recordtypes.yml | 8 ++ datasets/upsert/upsert_recordtypes.sql | 23 ++++ 13 files changed, 210 insertions(+), 26 deletions(-) create mode 100644 cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml create mode 100644 datasets/upsert/upsert_mapping_recordtypes.yml create mode 100644 datasets/upsert/upsert_recordtypes.sql diff --git a/cumulusci/tasks/bulkdata/extract.py b/cumulusci/tasks/bulkdata/extract.py index 7ffd594f76..6f6aa4b333 100644 --- a/cumulusci/tasks/bulkdata/extract.py +++ b/cumulusci/tasks/bulkdata/extract.py @@ -247,7 +247,10 @@ def strip_name_field(record): if "RecordTypeId" in mapping.fields: self._extract_record_types( - mapping.sf_object, mapping.get_source_record_type_table(), conn + mapping.sf_object, + mapping.get_source_record_type_table(), + conn, + self.org_config.is_person_accounts_enabled, ) self.session.commit() diff --git a/cumulusci/tasks/bulkdata/load.py b/cumulusci/tasks/bulkdata/load.py index 369612805e..9a4f82ecf8 100644 --- a/cumulusci/tasks/bulkdata/load.py +++ b/cumulusci/tasks/bulkdata/load.py @@ -367,7 +367,9 @@ def _load_record_types(self, sobjects, conn): """Persist record types for the given sObjects into the database.""" for sobject in sobjects: table_name = sobject + "_rt_target_mapping" - self._extract_record_types(sobject, table_name, conn) + self._extract_record_types( + sobject, table_name, conn, self.org_config.is_person_accounts_enabled + ) def _get_statics(self, mapping): """Return the static values (not column names) to be appended to diff --git a/cumulusci/tasks/bulkdata/query_transformers.py b/cumulusci/tasks/bulkdata/query_transformers.py index 01a240465d..cbc50e389a 100644 --- a/cumulusci/tasks/bulkdata/query_transformers.py +++ b/cumulusci/tasks/bulkdata/query_transformers.py @@ -1,7 +1,7 @@ import typing as T from functools import cached_property -from sqlalchemy import func, text +from sqlalchemy import and_, func, text from sqlalchemy.orm import Query, aliased from cumulusci.core.exceptions import BulkDataException @@ -134,10 +134,15 @@ def outerjoins_to_add(self): rt_source_table.columns.record_type_id == getattr(self.model, self.mapping.fields["RecordTypeId"]), ), + # Combination of IsPersonType and DeveloperName is unique ( rt_dest_table, - rt_dest_table.columns.developer_name - == rt_source_table.columns.developer_name, + and_( + rt_dest_table.columns.developer_name + == rt_source_table.columns.developer_name, + rt_dest_table.columns.is_person_type + == rt_source_table.columns.is_person_type, + ), ), ] diff --git a/cumulusci/tasks/bulkdata/tests/recordtypes.sql b/cumulusci/tasks/bulkdata/tests/recordtypes.sql index ed3af68f88..be1f7f370d 100644 --- a/cumulusci/tasks/bulkdata/tests/recordtypes.sql +++ b/cumulusci/tasks/bulkdata/tests/recordtypes.sql @@ -1,11 +1,12 @@ BEGIN TRANSACTION; CREATE TABLE "Account_rt_mapping" ( record_type_id VARCHAR(18) NOT NULL, - developer_name VARCHAR(255), + developer_name VARCHAR(255), + is_person_type BOOLEAN, PRIMARY KEY (record_type_id) ); -INSERT INTO "Account_rt_mapping" VALUES('012P0000000bCMdIAM','Organization'); -INSERT INTO "Account_rt_mapping" VALUES('012P0000000bCQqIAM','Subsidiary'); +INSERT INTO "Account_rt_mapping" VALUES('012P0000000bCMdIAM','Organization',0); +INSERT INTO "Account_rt_mapping" VALUES('012P0000000bCQqIAM','Subsidiary',0); CREATE TABLE accounts ( sf_id VARCHAR(255) NOT NULL, "Name" VARCHAR(255), diff --git a/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql b/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql index 8b39829847..1d23fc4a8d 100644 --- a/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql +++ b/cumulusci/tasks/bulkdata/tests/recordtypes_2.sql @@ -1,11 +1,12 @@ BEGIN TRANSACTION; CREATE TABLE Beta_rt_mapping ( record_type_id VARCHAR(18) NOT NULL, - developer_name VARCHAR(255), + developer_name VARCHAR(255), + is_person_type BOOLEAN, PRIMARY KEY (record_type_id) ); -INSERT INTO "Beta_rt_mapping" VALUES('012H40000003jCoIAI','recordtype2'); -INSERT INTO "Beta_rt_mapping" VALUES('012H40000003jCZIAY','recordtype1'); +INSERT INTO "Beta_rt_mapping" VALUES('012H40000003jCoIAI','recordtype2',0); +INSERT INTO "Beta_rt_mapping" VALUES('012H40000003jCZIAY','recordtype1',0); CREATE TABLE Beta ( id INTEGER NOT NULL, "Name" VARCHAR(255), diff --git a/cumulusci/tasks/bulkdata/tests/test_extract.py b/cumulusci/tasks/bulkdata/tests/test_extract.py index 96f480c368..a7a336a98a 100644 --- a/cumulusci/tasks/bulkdata/tests/test_extract.py +++ b/cumulusci/tasks/bulkdata/tests/test_extract.py @@ -471,6 +471,7 @@ def test_import_results__record_type_mapping(self): "Account", mapping.get_source_record_type_table(), task.session.connection.return_value, + task.org_config._is_person_accounts_enabled, ) def test_import_results__person_account_name_stripped(self): diff --git a/cumulusci/tasks/bulkdata/tests/test_load.py b/cumulusci/tasks/bulkdata/tests/test_load.py index 4609a0320d..c6e85fdcb2 100644 --- a/cumulusci/tasks/bulkdata/tests/test_load.py +++ b/cumulusci/tasks/bulkdata/tests/test_load.py @@ -1663,6 +1663,7 @@ def test_query_db__record_type_mapping(self): FROM accounts LEFT OUTER JOIN "Account_rt_mapping" ON "Account_rt_mapping".record_type_id = accounts."RecordTypeId" LEFT OUTER JOIN "Account_rt_target_mapping" ON "Account_rt_target_mapping".developer_name = "Account_rt_mapping".developer_name + AND "account_rt_target_mapping".is_person_type = "account_rt_mapping".is_person_type """, ) @@ -1675,6 +1676,7 @@ def test_query_db__record_type_mapping_table_from_tablename(self): FROM "Beta" LEFT OUTER JOIN "Beta_rt_mapping" ON "Beta_rt_mapping".record_type_id = "Beta"."RecordType" LEFT OUTER JOIN "Account_rt_target_mapping" ON "Account_rt_target_mapping".developer_name = "Beta_rt_mapping".developer_name + AND "Account_rt_target_mapping".is_person_type = "Beta_rt_mapping".is_person_type """, ) @@ -1716,11 +1718,12 @@ def test_load_record_types(self): conn = mock.Mock() task._extract_record_types = mock.Mock() + task.org_config._is_person_accounts_enabled = True task._load_record_types(["Account", "Contact"], conn) task._extract_record_types.assert_has_calls( [ - mock.call("Account", "Account_rt_target_mapping", conn), - mock.call("Contact", "Contact_rt_target_mapping", conn), + mock.call("Account", "Account_rt_target_mapping", conn, True), + mock.call("Contact", "Contact_rt_target_mapping", conn, True), ] ) diff --git a/cumulusci/tasks/bulkdata/tests/test_upsert.py b/cumulusci/tasks/bulkdata/tests/test_upsert.py index 8caa7ecb55..f9bf0a9374 100644 --- a/cumulusci/tasks/bulkdata/tests/test_upsert.py +++ b/cumulusci/tasks/bulkdata/tests/test_upsert.py @@ -3,6 +3,7 @@ import pytest import responses +import yaml from cumulusci.core.exceptions import BulkDataException from cumulusci.tasks.bulkdata import LoadData @@ -738,6 +739,111 @@ def test_simple_upsert_smart__native_field( } } + @responses.activate + def test_upsert_recordtype_same_developername_different_ispersontype( + self, create_task, cumulusci_test_repo_root, org_config, sf + ): + domain = org_config.get_domain() + ver = CURRENT_SF_API_VERSION + expected_number_of_records = 3 + responses.add( + method="GET", + url=f"https://{domain}/services/data/v{ver}/query/?q=SELECT+Id%2C+DeveloperName%2C+IsPersonType+FROM+RecordType+WHERE+SObjectType%3D%27Account%27", + status=200, + json={ + "totalSize": 4, + "done": True, + "records": [ + { + "Id": "0125j000000RqVkAAK", + "DeveloperName": "HH_Account", + "IsPersonType": False, + }, + { + "Id": "0125j000000RqVlAAK", + "DeveloperName": "Organization", + "IsPersonType": False, + }, + { + "Id": "0125j000000bo4yAAA", + "DeveloperName": "PersonAccount", + "IsPersonType": True, + }, + { + "Id": "0125j000000bo53AAA", + "DeveloperName": "PersonAccount", + "IsPersonType": False, + }, + ], + }, + ) + responses.add( + method="GET", + url=f"https://{domain}/services/data/v{ver}/limits/recordCount?sObjects=Account", + status=200, + json={"sObjects": [{"count": 3, "name": "Account"}]}, + ) + responses.add( + method="GET", + url=f"https://{domain}/services/data/v{ver}/query/?q=select+Id%2CAccountNumber+from+Account", + status=200, + json={ + "totalSize": 3, + "done": True, + "records": [ + {"Id": "0015j00001H0q4NAAR", "AccountNumber": "12345"}, + {"Id": "0015j00001H0q4OAAR", "AccountNumber": "456789"}, + {"Id": "0015j00001H0q7bAAB", "AccountNumber": "909098"}, + ], + }, + ) + with ( + cumulusci_test_repo_root + / "cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml" + ).open("r") as f: + body_accounts = yaml.safe_load(f)["response"]["body"]["string"] + responses.add( + method="GET", + url=f"https://{domain}/services/data/v{ver}/sobjects/Account/describe", + body=body_accounts, + status=200, + ) + with ( + cumulusci_test_repo_root + / "cumulusci/tests/shared_cassettes/GET_sobjects_Global_describe.yaml" + ).open("r") as f: + body_global = yaml.safe_load(f)["response"]["body"]["string"] + responses.add( + method="GET", + url=f"https://{domain}/services/data/v{ver}/sobjects", + body=body_global, + status=200, + ) + task = create_task( + LoadData, + { + "sql_path": cumulusci_test_repo_root + / "datasets/upsert/upsert_recordtypes.sql", + "mapping": cumulusci_test_repo_root + / "datasets/upsert/upsert_mapping_recordtypes.yml", + "set_recently_viewed": False, + }, + ) + task._update_credentials = mock.Mock() + task.sf = sf + task.bulk = mock.Mock() + task._init_mapping() + with task._init_db(): + task._expand_mapping() + mapping = task.mapping["Upsert Accounts"] + if "RecordTypeId" in mapping.fields: + conn = task.session.connection() + task._load_record_types([mapping.sf_object], conn) + task.session.commit() + _, query = task.configure_step(mapping) + # Assert no duplicate records are trying to be deployed + assert len(list(query)) == expected_number_of_records + @responses.activate def test_simple_upsert_smart__non_native_field( self, create_task, cumulusci_test_repo_root, org_config diff --git a/cumulusci/tasks/bulkdata/tests/test_utils.py b/cumulusci/tasks/bulkdata/tests/test_utils.py index fa890f8512..ad3e69b8e0 100644 --- a/cumulusci/tasks/bulkdata/tests/test_utils.py +++ b/cumulusci/tasks/bulkdata/tests/test_utils.py @@ -51,7 +51,13 @@ def test_extract_record_types(self): util.sf = mock.Mock() util.sf.query.return_value = { "totalSize": 1, - "records": [{"Id": "012000000000000", "DeveloperName": "Organization"}], + "records": [ + { + "Id": "012000000000000", + "DeveloperName": "Organization", + "IsPersonType": "0", + } + ], } util.logger = mock.Mock() util.metadata = mock.MagicMock() @@ -60,17 +66,19 @@ def test_extract_record_types(self): with mock.patch( "cumulusci.tasks.bulkdata.utils.sql_bulk_insert_from_records" ) as sql_bulk_insert_from_records: - util._extract_record_types("Account", "test_table", conn) + util._extract_record_types("Account", "test_table", conn, True) util.sf.query.assert_called_once_with( - "SELECT Id, DeveloperName FROM RecordType WHERE SObjectType='Account'" + "SELECT Id, DeveloperName, IsPersonType FROM RecordType WHERE SObjectType='Account'" ) - sql_bulk_insert_from_records.assert_called_once() + sql_bulk_insert_from_records.assert_called() call = sql_bulk_insert_from_records.call_args_list[0][1] assert call["connection"] == conn assert call["table"] == util.metadata.tables["test_table"] - assert call["columns"] == ["record_type_id", "developer_name"] - assert list(call["record_iterable"]) == [["012000000000000", "Organization"]] + assert call["columns"] == ["record_type_id", "developer_name", "is_person_type"] + assert list(call["record_iterable"]) == [ + ["012000000000000", "Organization", "0"] + ] def test_sql_bulk_insert_from_records__sqlite(self): engine, metadata = create_db_memory() diff --git a/cumulusci/tasks/bulkdata/utils.py b/cumulusci/tasks/bulkdata/utils.py index 315b941b01..ea09ba49df 100644 --- a/cumulusci/tasks/bulkdata/utils.py +++ b/cumulusci/tasks/bulkdata/utils.py @@ -6,7 +6,7 @@ from pathlib import Path from simple_salesforce import Salesforce -from sqlalchemy import Column, Integer, MetaData, Table, Unicode, inspect +from sqlalchemy import Boolean, Column, Integer, MetaData, Table, Unicode, inspect from sqlalchemy.engine.base import Connection from sqlalchemy.orm import Session, mapper @@ -29,16 +29,20 @@ def _create_record_type_table(self, table_name): rt_map_fields = [ Column("record_type_id", Unicode(18), primary_key=True), Column("developer_name", Unicode(255)), + Column("is_person_type", Boolean), ] rt_map_table = Table(table_name, self.metadata, *rt_map_fields) mapper(self.models[table_name], rt_map_table) - def _extract_record_types(self, sobject, tablename: str, conn): + def _extract_record_types( + self, sobject, tablename: str, conn, is_person_accounts_enabled: bool + ): """Query for Record Type information and persist it in the database.""" self.logger.info(f"Extracting Record Types for {sobject}") - query = ( - f"SELECT Id, DeveloperName FROM RecordType WHERE SObjectType='{sobject}'" - ) + if is_person_accounts_enabled: + query = f"SELECT Id, DeveloperName, IsPersonType FROM RecordType WHERE SObjectType='{sobject}'" + else: + query = f"SELECT Id, DeveloperName FROM RecordType WHERE SObjectType='{sobject}'" result = self.sf.query(query) @@ -46,9 +50,10 @@ def _extract_record_types(self, sobject, tablename: str, conn): sql_bulk_insert_from_records( connection=conn, table=self.metadata.tables[tablename], - columns=["record_type_id", "developer_name"], + columns=["record_type_id", "developer_name", "is_person_type"], record_iterable=( - [rt["Id"], rt["DeveloperName"]] for rt in result["records"] + [rt["Id"], rt["DeveloperName"], rt.get("IsPersonType", False)] + for rt in result["records"] ), ) diff --git a/cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml b/cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml new file mode 100644 index 0000000000..8c12990522 --- /dev/null +++ b/cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml @@ -0,0 +1,18 @@ +request: + method: GET + uri: https://orgname.my.salesforce.com/services/data/vxx.0/sobjects/Account/describe + body: null + headers: + Request-Headers: + - Elided +response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json;charset=UTF-8 + Others: Elided + body: + string: >- + {"actionOverrides":[],"activateable":false,"associateEntityType":null,"associateParentEntity":null,"childRelationships":[{"cascadeDelete":true,"childSObject":"AIInsightValue","deprecatedAndHidden":false,"field":"SobjectLookupValueId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AIRecordInsight","deprecatedAndHidden":false,"field":"TargetId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AcceptedEventRelation","deprecatedAndHidden":false,"field":"RelationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonAcceptedEventRelations","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Account","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ChildAccounts","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Account","deprecatedAndHidden":false,"field":"npe01__One2OneContact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe01__Organizations__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AccountChangeEvent","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AccountChangeEvent","deprecatedAndHidden":false,"field":"PersonContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountCleanInfo","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AccountCleanInfos","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountContactRole","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AccountContactRoles","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountContactRole","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonAccountContactRoles","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AccountContactRoleChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AccountContactRoleChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountFeed","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Feeds","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountHistory","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Histories","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountPartner","deprecatedAndHidden":false,"field":"AccountFromId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AccountPartnersFrom","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountPartner","deprecatedAndHidden":false,"field":"AccountToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AccountPartnersTo","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AccountShare","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Shares","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ActivityHistory","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ActivityHistories","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ActivityHistory","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonActivityHistories","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AgentWork","deprecatedAndHidden":false,"field":"WorkItemId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AlternativePaymentMethod","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AlternativePaymentMethods","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Asset","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Assets","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Asset","deprecatedAndHidden":false,"field":"AssetProvidedById","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ProvidedAssets","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Asset","deprecatedAndHidden":false,"field":"AssetServicedById","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ServicedAssets","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Asset","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonAssets","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AssetChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AssetChangeEvent","deprecatedAndHidden":false,"field":"AssetProvidedById","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AssetChangeEvent","deprecatedAndHidden":false,"field":"AssetServicedById","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AssetChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AssociatedLocation","deprecatedAndHidden":false,"field":"ParentRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AssociatedLocations","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"AttachedContentDocument","deprecatedAndHidden":false,"field":"LinkedEntityId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AttachedContentDocuments","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Attachment","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Attachments","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AuthorizationFormConsent","deprecatedAndHidden":false,"field":"ConsentGiverId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"AuthorizationFormConsents","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"AuthorizationFormConsent","deprecatedAndHidden":false,"field":"RelatedRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"RelatedAuthorizationFormConsents","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AuthorizationFormConsentChangeEvent","deprecatedAndHidden":false,"field":"ConsentGiverId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"AuthorizationFormConsentChangeEvent","deprecatedAndHidden":false,"field":"RelatedRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"CampaignMember","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonCampaignMembers","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"CampaignMember","deprecatedAndHidden":false,"field":"LeadOrContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"CampaignMemberChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"CardPaymentMethod","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"CardPaymentMethods","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Case","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Cases","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"Case","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonCases","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"CaseChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"CaseChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"CaseContactRole","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonCaseContactRoles","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"CollaborationGroupRecord","deprecatedAndHidden":false,"field":"RecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"RecordAssociatedGroups","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"CombinedAttachment","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"CombinedAttachments","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"CommSubscriptionConsent","deprecatedAndHidden":false,"field":"ConsentGiverId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"CommSubscriptionConsents","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"CommSubscriptionConsentChangeEvent","deprecatedAndHidden":false,"field":"ConsentGiverId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Contact","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Contacts","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Contact","deprecatedAndHidden":false,"field":"npsp__Primary_Affiliation__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__PrimaryAffiliatedContacts__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactChangeEvent","deprecatedAndHidden":false,"field":"ReportsToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContactCleanInfo","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonContactCleanInfos","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContactPointAddress","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ContactPointAddresses","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactPointAddressChangeEvent","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContactPointEmail","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ContactPointEmails","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactPointEmailChangeEvent","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContactPointPhone","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ContactPointPhones","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactPointPhoneChangeEvent","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactRequest","deprecatedAndHidden":false,"field":"WhatId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ContactRequests","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContactRequest","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonContactRequests","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContentDistribution","deprecatedAndHidden":false,"field":"RelatedRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContentDocumentLink","deprecatedAndHidden":false,"field":"LinkedEntityId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ContentDocumentLinks","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContentDocumentLinkChangeEvent","deprecatedAndHidden":false,"field":"LinkedEntityId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContentVersion","deprecatedAndHidden":false,"field":"FirstPublishLocationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContentVersionChangeEvent","deprecatedAndHidden":false,"field":"FirstPublishLocationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Contract","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Contracts","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"Contract","deprecatedAndHidden":false,"field":"CustomerSignedId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonContractsSigned","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"ContractChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ContractChangeEvent","deprecatedAndHidden":false,"field":"CustomerSignedId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ContractContactRole","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonContractContactRoles","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ConversationParticipant","deprecatedAndHidden":false,"field":"ParticipantEntityId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonConversationParticipants","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"CreditMemo","deprecatedAndHidden":false,"field":"BillToContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonCreditMemos","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"CreditMemo","deprecatedAndHidden":false,"field":"BillingAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"CreditMemos","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"DeclinedEventRelation","deprecatedAndHidden":false,"field":"RelationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonDeclinedEventRelations","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"DigitalWallet","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"DigitalWallets","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"DuplicateRecordItem","deprecatedAndHidden":false,"field":"RecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"DuplicateRecordItems","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"EmailMessage","deprecatedAndHidden":false,"field":"RelatedToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Emails","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"EmailMessageChangeEvent","deprecatedAndHidden":false,"field":"RelatedToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"EmailMessageRelation","deprecatedAndHidden":false,"field":"RelationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonEmailMessageRelations","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"EmailStatus","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonEmailStatuses","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Entitlement","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Entitlements","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"EntitlementChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"EntitlementContact","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonEntitlementContacts","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"EntitySubscription","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"FeedSubscriptionsForEntity","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Event","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Event","deprecatedAndHidden":false,"field":"WhatId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Events","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Event","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonEvents","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"EventChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"EventChangeEvent","deprecatedAndHidden":false,"field":"WhatId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"EventChangeEvent","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"EventRelationChangeEvent","deprecatedAndHidden":false,"field":"RelationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FeedComment","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"FeedItem","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FinanceBalanceSnapshot","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"FinanceBalanceSnapshots","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FinanceBalanceSnapshotChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FinanceTransaction","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"FinanceTransactions","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FinanceTransactionChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FlowExecutionErrorEvent","deprecatedAndHidden":false,"field":"ContextRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FlowOrchestrationWorkItem","deprecatedAndHidden":false,"field":"RelatedRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"FlowOrchestrationWorkItems","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"FlowRecordRelation","deprecatedAndHidden":false,"field":"RelatedRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Invoice","deprecatedAndHidden":false,"field":"BillToContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonInvoices","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Invoice","deprecatedAndHidden":false,"field":"BillingAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Invoices","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Lead","deprecatedAndHidden":false,"field":"ConvertedAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Lead","deprecatedAndHidden":false,"field":"ConvertedContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"LeadChangeEvent","deprecatedAndHidden":false,"field":"ConvertedAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"LeadChangeEvent","deprecatedAndHidden":false,"field":"ConvertedContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ListEmailIndividualRecipient","deprecatedAndHidden":false,"field":"RecipientId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonListEmailIndividualRecipients","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"MatchingInformation","deprecatedAndHidden":false,"field":"SFDCIdId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"MessagingEndUser","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"MessagingEndUsers","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"MessagingEndUser","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonMessagingEndUsers","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"MessagingSession","deprecatedAndHidden":false,"field":"EndUserAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"MessagingSessions","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"MessagingSession","deprecatedAndHidden":false,"field":"EndUserContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonMessagingSessions","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Note","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Notes","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"NoteAndAttachment","deprecatedAndHidden":false,"field":"ParentId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"NotesAndAttachments","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"OpenActivity","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"OpenActivities","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"OpenActivity","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonOpenActivities","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Opportunity","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Opportunities","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Opportunity","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonOpportunities","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Opportunity","deprecatedAndHidden":false,"field":"npsp__Honoree_Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Honoree_Opportunities__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Opportunity","deprecatedAndHidden":false,"field":"npsp__Matching_Gift_Account__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Opportunities__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Opportunity","deprecatedAndHidden":false,"field":"npsp__Notification_Recipient_Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Notification_Opportunities__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Opportunity","deprecatedAndHidden":false,"field":"npsp__Primary_Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Opportunities__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OpportunityChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OpportunityChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"OpportunityContactRole","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonOpportunityContactRoles","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OpportunityContactRoleChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"OpportunityPartner","deprecatedAndHidden":false,"field":"AccountToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"OpportunityPartnersTo","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Order","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Orders","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"Order","deprecatedAndHidden":false,"field":"BillToContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":true},{"cascadeDelete":false,"childSObject":"Order","deprecatedAndHidden":false,"field":"CustomerAuthorizedById","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":true},{"cascadeDelete":false,"childSObject":"Order","deprecatedAndHidden":false,"field":"ShipToContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":true},{"cascadeDelete":false,"childSObject":"OrderChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OrderChangeEvent","deprecatedAndHidden":false,"field":"BillToContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OrderChangeEvent","deprecatedAndHidden":false,"field":"CustomerAuthorizedById","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OrderChangeEvent","deprecatedAndHidden":false,"field":"ShipToContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OutgoingEmail","deprecatedAndHidden":false,"field":"RelatedToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OutgoingEmail","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"OutgoingEmailRelation","deprecatedAndHidden":false,"field":"RelationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonOutgoingEmailRelations","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Partner","deprecatedAndHidden":false,"field":"AccountFromId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PartnersFrom","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Partner","deprecatedAndHidden":false,"field":"AccountToId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PartnersTo","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Payment","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Payments","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"PaymentAuthAdjustment","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PaymentAuthAdjustments","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"PaymentAuthorization","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PaymentAuthorizations","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"PaymentLineInvoice","deprecatedAndHidden":false,"field":"AssociatedAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PaymentLinesInvoice","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"PaymentMethod","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"PendingServiceRouting","deprecatedAndHidden":false,"field":"WorkItemId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"PendingServiceRoutingInteractionInfo","deprecatedAndHidden":false,"field":"PrimaryRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"PendingServiceRoutingInteractionInfo","deprecatedAndHidden":false,"field":"TargetObjectId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ProcessInstance","deprecatedAndHidden":false,"field":"TargetObjectId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ProcessInstances","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ProcessInstanceChangeEvent","deprecatedAndHidden":false,"field":"TargetObjectId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ProcessInstanceHistory","deprecatedAndHidden":false,"field":"TargetObjectId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ProcessSteps","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"RecordAction","deprecatedAndHidden":false,"field":"RecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"RecordActions","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"RecordActionHistory","deprecatedAndHidden":false,"field":"ParentRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"RecordActionHistories","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Refund","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Refunds","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"RefundLinePayment","deprecatedAndHidden":false,"field":"AssociatedAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"RefundLinePayments","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"ScorecardAssociation","deprecatedAndHidden":false,"field":"TargetEntityId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ScorecardAssociations","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ServiceContract","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"ServiceContracts","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"ServiceContract","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonServiceContracts","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"ServiceContractChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"ServiceContractChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"Task","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Task","deprecatedAndHidden":false,"field":"WhatId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Tasks","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"Task","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonTasks","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"TaskChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"TaskChangeEvent","deprecatedAndHidden":false,"field":"WhatId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"TaskChangeEvent","deprecatedAndHidden":false,"field":"WhoId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"TopicAssignment","deprecatedAndHidden":false,"field":"EntityId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"TopicAssignments","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"UndecidedEventRelation","deprecatedAndHidden":false,"field":"RelationId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonUndecidedEventRelations","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"User","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"Users","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"User","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonUsers","restrictedDelete":true},{"cascadeDelete":false,"childSObject":"UserChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"UserChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":true,"childSObject":"UserEmailPreferredPerson","deprecatedAndHidden":false,"field":"PersonRecordId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonPersonRecord","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"UserPrioritizedRecord","deprecatedAndHidden":false,"field":"TargetId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"UserRole","deprecatedAndHidden":false,"field":"PortalAccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"WorkOrder","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"WorkOrders","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"WorkOrder","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"PersonWorkOrders","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"WorkOrderChangeEvent","deprecatedAndHidden":false,"field":"AccountId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"WorkOrderChangeEvent","deprecatedAndHidden":false,"field":"ContactId","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":null,"restrictedDelete":false},{"cascadeDelete":false,"childSObject":"foodbank_spidy__Delivery__c","deprecatedAndHidden":false,"field":"foodbank_spidy__Account__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"foodbank_spidy__Deliveries__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npe03__Recurring_Donation__c","deprecatedAndHidden":false,"field":"npe03__Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe03__R00N80000002bOmREAU__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npe03__Recurring_Donation__c","deprecatedAndHidden":false,"field":"npe03__Organization__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe03__RecurringDonations__r","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"npe4__Relationship__c","deprecatedAndHidden":false,"field":"npe4__Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe4__Relationships__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npe4__Relationship__c","deprecatedAndHidden":false,"field":"npe4__RelatedContact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe4__Relationships1__pr","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"npe5__Affiliation__c","deprecatedAndHidden":false,"field":"npe5__Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe5__Affiliations__pr","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"npe5__Affiliation__c","deprecatedAndHidden":false,"field":"npe5__Organization__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npe5__Affiliations__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__Account_Soft_Credit__c","deprecatedAndHidden":false,"field":"npsp__Account__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Account_Soft_Credits__r","restrictedDelete":true},{"cascadeDelete":true,"childSObject":"npsp__Address__c","deprecatedAndHidden":false,"field":"npsp__Household_Account__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Addresses__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__DataImport__c","deprecatedAndHidden":false,"field":"npsp__Account1Imported__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Data_Imports__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__DataImport__c","deprecatedAndHidden":false,"field":"npsp__Account2Imported__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Data_Imports1__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__DataImport__c","deprecatedAndHidden":false,"field":"npsp__Contact1Imported__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Data_Imports__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__DataImport__c","deprecatedAndHidden":false,"field":"npsp__Contact2Imported__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Data_Imports1__pr","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__DataImport__c","deprecatedAndHidden":false,"field":"npsp__HouseholdAccountImported__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__NPSP_Data_Imports__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__Engagement_Plan__c","deprecatedAndHidden":false,"field":"npsp__Account__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Action_Plans__r","restrictedDelete":false},{"cascadeDelete":false,"childSObject":"npsp__Engagement_Plan__c","deprecatedAndHidden":false,"field":"npsp__Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Action_Plans__pr","restrictedDelete":false},{"cascadeDelete":true,"childSObject":"npsp__Partial_Soft_Credit__c","deprecatedAndHidden":false,"field":"npsp__Contact__c","junctionIdListNames":[],"junctionReferenceTo":[],"relationshipName":"npsp__Partial_Soft_Credits__pr","restrictedDelete":false}],"compactLayoutable":true,"createable":true,"custom":false,"customSetting":false,"deepCloneable":false,"defaultImplementation":null,"deletable":true,"deprecatedAndHidden":false,"extendedBy":null,"extendsInterfaces":null,"feedEnabled":true,"fields":[{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":true,"inlineHelpText":null,"label":"Account ID","length":18,"mask":null,"maskType":null,"name":"Id","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"id","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Deleted","length":0,"mask":null,"maskType":null,"name":"IsDeleted","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Master Record ID","length":18,"mask":null,"maskType":null,"name":"MasterRecordId","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["Account"],"relationshipName":"MasterRecord","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"Name","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"switchablepersonname","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Name","length":255,"mask":null,"maskType":null,"name":"Name","nameField":true,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"Name","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"switchablepersonname","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Name","length":80,"mask":null,"maskType":null,"name":"LastName","nameField":true,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"Name","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"switchablepersonname","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"First Name","length":40,"mask":null,"maskType":null,"name":"FirstName","nameField":true,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"Name","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"switchablepersonname","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Salutation","length":40,"mask":null,"maskType":null,"name":"Salutation","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[{"active":true,"defaultValue":false,"label":"Mr.","validFor":null,"value":"Mr."},{"active":true,"defaultValue":false,"label":"Ms.","validFor":null,"value":"Ms."},{"active":true,"defaultValue":false,"label":"Mrs.","validFor":null,"value":"Mrs."},{"active":true,"defaultValue":false,"label":"Dr.","validFor":null,"value":"Dr."},{"active":true,"defaultValue":false,"label":"Prof.","validFor":null,"value":"Prof."},{"active":true,"defaultValue":false,"label":"Mx.","validFor":null,"value":"Mx."}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Type","length":255,"mask":null,"maskType":null,"name":"Type","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Prospect","validFor":null,"value":"Prospect"},{"active":true,"defaultValue":false,"label":"Customer - Direct","validFor":null,"value":"Customer - Direct"},{"active":true,"defaultValue":false,"label":"Customer - Channel","validFor":null,"value":"Customer - Channel"},{"active":true,"defaultValue":false,"label":"Channel Partner / Reseller","validFor":null,"value":"Channel Partner / Reseller"},{"active":true,"defaultValue":false,"label":"Installation Partner","validFor":null,"value":"Installation Partner"},{"active":true,"defaultValue":false,"label":"Technology Partner","validFor":null,"value":"Technology Partner"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"},{"active":true,"defaultValue":false,"label":"Pending","validFor":null,"value":"Pending"},{"active":true,"defaultValue":false,"label":"Customer","validFor":null,"value":"Customer"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Record Type ID","length":18,"mask":null,"maskType":null,"name":"RecordTypeId","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["RecordType"],"relationshipName":"RecordType","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Parent Account ID","length":18,"mask":null,"maskType":null,"name":"ParentId","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["Account"],"relationshipName":"Parent","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Street","length":255,"mask":null,"maskType":null,"name":"BillingStreet","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing City","length":40,"mask":null,"maskType":null,"name":"BillingCity","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing State/Province","length":80,"mask":null,"maskType":null,"name":"BillingState","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Zip/Postal Code","length":20,"mask":null,"maskType":null,"name":"BillingPostalCode","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Country","length":80,"mask":null,"maskType":null,"name":"BillingCountry","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Latitude","length":0,"mask":null,"maskType":null,"name":"BillingLatitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Longitude","length":0,"mask":null,"maskType":null,"name":"BillingLongitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"BillingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Geocode Accuracy","length":40,"mask":null,"maskType":null,"name":"BillingGeocodeAccuracy","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Address","validFor":null,"value":"Address"},{"active":true,"defaultValue":false,"label":"NearAddress","validFor":null,"value":"NearAddress"},{"active":true,"defaultValue":false,"label":"Block","validFor":null,"value":"Block"},{"active":true,"defaultValue":false,"label":"Street","validFor":null,"value":"Street"},{"active":true,"defaultValue":false,"label":"ExtendedZip","validFor":null,"value":"ExtendedZip"},{"active":true,"defaultValue":false,"label":"Zip","validFor":null,"value":"Zip"},{"active":true,"defaultValue":false,"label":"Neighborhood","validFor":null,"value":"Neighborhood"},{"active":true,"defaultValue":false,"label":"City","validFor":null,"value":"City"},{"active":true,"defaultValue":false,"label":"County","validFor":null,"value":"County"},{"active":true,"defaultValue":false,"label":"State","validFor":null,"value":"State"},{"active":true,"defaultValue":false,"label":"Unknown","validFor":null,"value":"Unknown"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":true,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Billing Address","length":0,"mask":null,"maskType":null,"name":"BillingAddress","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":true,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"urn:address","sortable":false,"type":"address","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Street","length":255,"mask":null,"maskType":null,"name":"ShippingStreet","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping City","length":40,"mask":null,"maskType":null,"name":"ShippingCity","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping State/Province","length":80,"mask":null,"maskType":null,"name":"ShippingState","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Zip/Postal Code","length":20,"mask":null,"maskType":null,"name":"ShippingPostalCode","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Country","length":80,"mask":null,"maskType":null,"name":"ShippingCountry","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Latitude","length":0,"mask":null,"maskType":null,"name":"ShippingLatitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Longitude","length":0,"mask":null,"maskType":null,"name":"ShippingLongitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"ShippingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Geocode Accuracy","length":40,"mask":null,"maskType":null,"name":"ShippingGeocodeAccuracy","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Address","validFor":null,"value":"Address"},{"active":true,"defaultValue":false,"label":"NearAddress","validFor":null,"value":"NearAddress"},{"active":true,"defaultValue":false,"label":"Block","validFor":null,"value":"Block"},{"active":true,"defaultValue":false,"label":"Street","validFor":null,"value":"Street"},{"active":true,"defaultValue":false,"label":"ExtendedZip","validFor":null,"value":"ExtendedZip"},{"active":true,"defaultValue":false,"label":"Zip","validFor":null,"value":"Zip"},{"active":true,"defaultValue":false,"label":"Neighborhood","validFor":null,"value":"Neighborhood"},{"active":true,"defaultValue":false,"label":"City","validFor":null,"value":"City"},{"active":true,"defaultValue":false,"label":"County","validFor":null,"value":"County"},{"active":true,"defaultValue":false,"label":"State","validFor":null,"value":"State"},{"active":true,"defaultValue":false,"label":"Unknown","validFor":null,"value":"Unknown"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":true,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Shipping Address","length":0,"mask":null,"maskType":null,"name":"ShippingAddress","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":true,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"urn:address","sortable":false,"type":"address","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Phone","length":40,"mask":null,"maskType":null,"name":"Phone","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Fax","length":40,"mask":null,"maskType":null,"name":"Fax","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Number","length":40,"mask":null,"maskType":null,"name":"AccountNumber","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Website","length":255,"mask":null,"maskType":null,"name":"Website","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"url","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"imageurl","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Photo URL","length":255,"mask":null,"maskType":null,"name":"PhotoUrl","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"url","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"SIC Code","length":20,"mask":null,"maskType":null,"name":"Sic","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Industry","length":255,"mask":null,"maskType":null,"name":"Industry","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Agriculture","validFor":null,"value":"Agriculture"},{"active":true,"defaultValue":false,"label":"Apparel","validFor":null,"value":"Apparel"},{"active":true,"defaultValue":false,"label":"Banking","validFor":null,"value":"Banking"},{"active":true,"defaultValue":false,"label":"Biotechnology","validFor":null,"value":"Biotechnology"},{"active":true,"defaultValue":false,"label":"Chemicals","validFor":null,"value":"Chemicals"},{"active":true,"defaultValue":false,"label":"Communications","validFor":null,"value":"Communications"},{"active":true,"defaultValue":false,"label":"Construction","validFor":null,"value":"Construction"},{"active":true,"defaultValue":false,"label":"Consulting","validFor":null,"value":"Consulting"},{"active":true,"defaultValue":false,"label":"Education","validFor":null,"value":"Education"},{"active":true,"defaultValue":false,"label":"Electronics","validFor":null,"value":"Electronics"},{"active":true,"defaultValue":false,"label":"Energy","validFor":null,"value":"Energy"},{"active":true,"defaultValue":false,"label":"Engineering","validFor":null,"value":"Engineering"},{"active":true,"defaultValue":false,"label":"Entertainment","validFor":null,"value":"Entertainment"},{"active":true,"defaultValue":false,"label":"Environmental","validFor":null,"value":"Environmental"},{"active":true,"defaultValue":false,"label":"Finance","validFor":null,"value":"Finance"},{"active":true,"defaultValue":false,"label":"Food & Beverage","validFor":null,"value":"Food & Beverage"},{"active":true,"defaultValue":false,"label":"Government","validFor":null,"value":"Government"},{"active":true,"defaultValue":false,"label":"Healthcare","validFor":null,"value":"Healthcare"},{"active":true,"defaultValue":false,"label":"Hospitality","validFor":null,"value":"Hospitality"},{"active":true,"defaultValue":false,"label":"Insurance","validFor":null,"value":"Insurance"},{"active":true,"defaultValue":false,"label":"Machinery","validFor":null,"value":"Machinery"},{"active":true,"defaultValue":false,"label":"Manufacturing","validFor":null,"value":"Manufacturing"},{"active":true,"defaultValue":false,"label":"Media","validFor":null,"value":"Media"},{"active":true,"defaultValue":false,"label":"Not For Profit","validFor":null,"value":"Not For Profit"},{"active":true,"defaultValue":false,"label":"Recreation","validFor":null,"value":"Recreation"},{"active":true,"defaultValue":false,"label":"Retail","validFor":null,"value":"Retail"},{"active":true,"defaultValue":false,"label":"Shipping","validFor":null,"value":"Shipping"},{"active":true,"defaultValue":false,"label":"Technology","validFor":null,"value":"Technology"},{"active":true,"defaultValue":false,"label":"Telecommunications","validFor":null,"value":"Telecommunications"},{"active":true,"defaultValue":false,"label":"Transportation","validFor":null,"value":"Transportation"},{"active":true,"defaultValue":false,"label":"Utilities","validFor":null,"value":"Utilities"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Annual Revenue","length":0,"mask":null,"maskType":null,"name":"AnnualRevenue","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":8,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Employees","length":0,"mask":null,"maskType":null,"name":"NumberOfEmployees","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:int","sortable":true,"type":"int","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Ownership","length":255,"mask":null,"maskType":null,"name":"Ownership","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Public","validFor":null,"value":"Public"},{"active":true,"defaultValue":false,"label":"Private","validFor":null,"value":"Private"},{"active":true,"defaultValue":false,"label":"Subsidiary","validFor":null,"value":"Subsidiary"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Ticker Symbol","length":20,"mask":null,"maskType":null,"name":"TickerSymbol","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":96000,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":false,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Description","length":32000,"mask":null,"maskType":null,"name":"Description","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":false,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Rating","length":255,"mask":null,"maskType":null,"name":"Rating","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Hot","validFor":null,"value":"Hot"},{"active":true,"defaultValue":false,"label":"Warm","validFor":null,"value":"Warm"},{"active":true,"defaultValue":false,"label":"Cold","validFor":null,"value":"Cold"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Site","length":80,"mask":null,"maskType":null,"name":"Site","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Owner ID","length":18,"mask":null,"maskType":null,"name":"OwnerId","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["User"],"relationshipName":"Owner","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Created Date","length":0,"mask":null,"maskType":null,"name":"CreatedDate","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Created By ID","length":18,"mask":null,"maskType":null,"name":"CreatedById","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["User"],"relationshipName":"CreatedBy","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Modified Date","length":0,"mask":null,"maskType":null,"name":"LastModifiedDate","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Modified By ID","length":18,"mask":null,"maskType":null,"name":"LastModifiedById","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["User"],"relationshipName":"LastModifiedBy","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"System Modstamp","length":0,"mask":null,"maskType":null,"name":"SystemModstamp","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Activity","length":0,"mask":null,"maskType":null,"name":"LastActivityDate","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Viewed Date","length":0,"mask":null,"maskType":null,"name":"LastViewedDate","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Referenced Date","length":0,"mask":null,"maskType":null,"name":"LastReferencedDate","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Contact ID","length":18,"mask":null,"maskType":null,"name":"PersonContactId","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["Contact"],"relationshipName":"PersonContact","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Is Person Account","length":0,"mask":null,"maskType":null,"name":"IsPersonAccount","nameField":false,"namePointing":false,"nillable":false,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Street","length":255,"mask":null,"maskType":null,"name":"PersonMailingStreet","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing City","length":40,"mask":null,"maskType":null,"name":"PersonMailingCity","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing State/Province","length":80,"mask":null,"maskType":null,"name":"PersonMailingState","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Zip/Postal Code","length":20,"mask":null,"maskType":null,"name":"PersonMailingPostalCode","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Country","length":80,"mask":null,"maskType":null,"name":"PersonMailingCountry","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Latitude","length":0,"mask":null,"maskType":null,"name":"PersonMailingLatitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Longitude","length":0,"mask":null,"maskType":null,"name":"PersonMailingLongitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonMailingAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Geocode Accuracy","length":40,"mask":null,"maskType":null,"name":"PersonMailingGeocodeAccuracy","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Address","validFor":null,"value":"Address"},{"active":true,"defaultValue":false,"label":"NearAddress","validFor":null,"value":"NearAddress"},{"active":true,"defaultValue":false,"label":"Block","validFor":null,"value":"Block"},{"active":true,"defaultValue":false,"label":"Street","validFor":null,"value":"Street"},{"active":true,"defaultValue":false,"label":"ExtendedZip","validFor":null,"value":"ExtendedZip"},{"active":true,"defaultValue":false,"label":"Zip","validFor":null,"value":"Zip"},{"active":true,"defaultValue":false,"label":"Neighborhood","validFor":null,"value":"Neighborhood"},{"active":true,"defaultValue":false,"label":"City","validFor":null,"value":"City"},{"active":true,"defaultValue":false,"label":"County","validFor":null,"value":"County"},{"active":true,"defaultValue":false,"label":"State","validFor":null,"value":"State"},{"active":true,"defaultValue":false,"label":"Unknown","validFor":null,"value":"Unknown"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":true,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mailing Address","length":0,"mask":null,"maskType":null,"name":"PersonMailingAddress","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":true,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"urn:address","sortable":false,"type":"address","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Street","length":255,"mask":null,"maskType":null,"name":"PersonOtherStreet","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other City","length":40,"mask":null,"maskType":null,"name":"PersonOtherCity","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other State/Province","length":80,"mask":null,"maskType":null,"name":"PersonOtherState","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Zip/Postal Code","length":20,"mask":null,"maskType":null,"name":"PersonOtherPostalCode","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Country","length":80,"mask":null,"maskType":null,"name":"PersonOtherCountry","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Latitude","length":0,"mask":null,"maskType":null,"name":"PersonOtherLatitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Longitude","length":0,"mask":null,"maskType":null,"name":"PersonOtherLongitude","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":15,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":"PersonOtherAddress","controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Geocode Accuracy","length":40,"mask":null,"maskType":null,"name":"PersonOtherGeocodeAccuracy","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Address","validFor":null,"value":"Address"},{"active":true,"defaultValue":false,"label":"NearAddress","validFor":null,"value":"NearAddress"},{"active":true,"defaultValue":false,"label":"Block","validFor":null,"value":"Block"},{"active":true,"defaultValue":false,"label":"Street","validFor":null,"value":"Street"},{"active":true,"defaultValue":false,"label":"ExtendedZip","validFor":null,"value":"ExtendedZip"},{"active":true,"defaultValue":false,"label":"Zip","validFor":null,"value":"Zip"},{"active":true,"defaultValue":false,"label":"Neighborhood","validFor":null,"value":"Neighborhood"},{"active":true,"defaultValue":false,"label":"City","validFor":null,"value":"City"},{"active":true,"defaultValue":false,"label":"County","validFor":null,"value":"County"},{"active":true,"defaultValue":false,"label":"State","validFor":null,"value":"State"},{"active":true,"defaultValue":false,"label":"Unknown","validFor":null,"value":"Unknown"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":true,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Address","length":0,"mask":null,"maskType":null,"name":"PersonOtherAddress","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":true,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"urn:address","sortable":false,"type":"address","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Mobile","length":40,"mask":null,"maskType":null,"name":"PersonMobilePhone","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Home Phone","length":40,"mask":null,"maskType":null,"name":"PersonHomePhone","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Other Phone","length":40,"mask":null,"maskType":null,"name":"PersonOtherPhone","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Asst. Phone","length":40,"mask":null,"maskType":null,"name":"PersonAssistantPhone","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Email","length":80,"mask":null,"maskType":null,"name":"PersonEmail","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"email","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Title","length":80,"mask":null,"maskType":null,"name":"PersonTitle","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Department","length":80,"mask":null,"maskType":null,"name":"PersonDepartment","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Assistant","length":40,"mask":null,"maskType":null,"name":"PersonAssistantName","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Lead Source","length":255,"mask":null,"maskType":null,"name":"PersonLeadSource","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Web","validFor":null,"value":"Web"},{"active":true,"defaultValue":false,"label":"Phone Inquiry","validFor":null,"value":"Phone Inquiry"},{"active":true,"defaultValue":false,"label":"Partner Referral","validFor":null,"value":"Partner Referral"},{"active":true,"defaultValue":false,"label":"Purchased List","validFor":null,"value":"Purchased List"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Birthdate","length":0,"mask":null,"maskType":null,"name":"PersonBirthdate","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Email Opt Out","length":0,"mask":null,"maskType":null,"name":"PersonHasOptedOutOfEmail","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Fax Opt Out","length":0,"mask":null,"maskType":null,"name":"PersonHasOptedOutOfFax","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Do Not Call","length":0,"mask":null,"maskType":null,"name":"PersonDoNotCall","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Stay-in-Touch Request Date","length":0,"mask":null,"maskType":null,"name":"PersonLastCURequestDate","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Stay-in-Touch Save Date","length":0,"mask":null,"maskType":null,"name":"PersonLastCUUpdateDate","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Email Bounced Reason","length":255,"mask":null,"maskType":null,"name":"PersonEmailBouncedReason","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Email Bounced Date","length":0,"mask":null,"maskType":null,"name":"PersonEmailBouncedDate","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:dateTime","sortable":true,"type":"datetime","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Individual ID","length":18,"mask":null,"maskType":null,"name":"PersonIndividualId","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["Individual"],"relationshipName":"PersonIndividual","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Pronouns","length":255,"mask":null,"maskType":null,"name":"PersonPronouns","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"He/Him","validFor":null,"value":"He/Him"},{"active":true,"defaultValue":false,"label":"She/Her","validFor":null,"value":"She/Her"},{"active":true,"defaultValue":false,"label":"They/Them","validFor":null,"value":"They/Them"},{"active":true,"defaultValue":false,"label":"He/They","validFor":null,"value":"He/They"},{"active":true,"defaultValue":false,"label":"She/They","validFor":null,"value":"She/They"},{"active":true,"defaultValue":false,"label":"Not Listed","validFor":null,"value":"Not Listed"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Gender Identity","length":255,"mask":null,"maskType":null,"name":"PersonGenderIdentity","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Male","validFor":null,"value":"Male"},{"active":true,"defaultValue":false,"label":"Female","validFor":null,"value":"Female"},{"active":true,"defaultValue":false,"label":"Nonbinary","validFor":null,"value":"Nonbinary"},{"active":true,"defaultValue":false,"label":"Not Listed","validFor":null,"value":"Not Listed"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Data.com Key","length":20,"mask":null,"maskType":null,"name":"Jigsaw","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":60,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Jigsaw Company ID","length":20,"mask":null,"maskType":null,"name":"JigsawCompanyId","nameField":false,"namePointing":false,"nillable":true,"permissionable":false,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":"JigsawCompany","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Clean Status","length":40,"mask":null,"maskType":null,"name":"CleanStatus","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"In Sync","validFor":null,"value":"Matched"},{"active":true,"defaultValue":false,"label":"Different","validFor":null,"value":"Different"},{"active":true,"defaultValue":false,"label":"Reviewed","validFor":null,"value":"Acknowledged"},{"active":true,"defaultValue":false,"label":"Not Found","validFor":null,"value":"NotFound"},{"active":true,"defaultValue":false,"label":"Inactive","validFor":null,"value":"Inactive"},{"active":true,"defaultValue":false,"label":"Not Compared","validFor":null,"value":"Pending"},{"active":true,"defaultValue":false,"label":"Select Match","validFor":null,"value":"SelectMatch"},{"active":true,"defaultValue":false,"label":"Skipped","validFor":null,"value":"Skipped"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":true,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Account Source","length":255,"mask":null,"maskType":null,"name":"AccountSource","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Web","validFor":null,"value":"Web"},{"active":true,"defaultValue":false,"label":"Phone Inquiry","validFor":null,"value":"Phone Inquiry"},{"active":true,"defaultValue":false,"label":"Partner Referral","validFor":null,"value":"Partner Referral"},{"active":true,"defaultValue":false,"label":"Purchased List","validFor":null,"value":"Purchased List"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":27,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"D-U-N-S Number","length":9,"mask":null,"maskType":null,"name":"DunsNumber","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Tradestyle","length":255,"mask":null,"maskType":null,"name":"Tradestyle","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":24,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"NAICS Code","length":8,"mask":null,"maskType":null,"name":"NaicsCode","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":360,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"NAICS Description","length":120,"mask":null,"maskType":null,"name":"NaicsDesc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":12,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Year Started","length":4,"mask":null,"maskType":null,"name":"YearStarted","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"SIC Description","length":80,"mask":null,"maskType":null,"name":"SicDesc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":false,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"D&B Company ID","length":18,"mask":null,"maskType":null,"name":"DandbCompanyId","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["DandBCompany"],"relationshipName":"DandbCompany","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Customer Priority","length":255,"mask":null,"maskType":null,"name":"CustomerPriority__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"High","validFor":null,"value":"High"},{"active":true,"defaultValue":false,"label":"Low","validFor":null,"value":"Low"},{"active":true,"defaultValue":false,"label":"Medium","validFor":null,"value":"Medium"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"SLA","length":255,"mask":null,"maskType":null,"name":"SLA__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Gold","validFor":null,"value":"Gold"},{"active":true,"defaultValue":false,"label":"Silver","validFor":null,"value":"Silver"},{"active":true,"defaultValue":false,"label":"Platinum","validFor":null,"value":"Platinum"},{"active":true,"defaultValue":false,"label":"Bronze","validFor":null,"value":"Bronze"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Active","length":255,"mask":null,"maskType":null,"name":"Active__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"No","validFor":null,"value":"No"},{"active":true,"defaultValue":false,"label":"Yes","validFor":null,"value":"Yes"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Number of Locations","length":0,"mask":null,"maskType":null,"name":"NumberofLocations__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":3,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Upsell Opportunity","length":255,"mask":null,"maskType":null,"name":"UpsellOpportunity__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Maybe","validFor":null,"value":"Maybe"},{"active":true,"defaultValue":false,"label":"No","validFor":null,"value":"No"},{"active":true,"defaultValue":false,"label":"Yes","validFor":null,"value":"Yes"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":30,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"SLA Serial Number","length":10,"mask":null,"maskType":null,"name":"SLASerialNumber__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"SLA Expiration Date","length":0,"mask":null,"maskType":null,"name":"SLAExpirationDate__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The primary contact for the Account, or the One2One contact for a One2One account.","label":"Primary Contact","length":18,"mask":null,"maskType":null,"name":"npe01__One2OneContact__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["Contact"],"relationshipName":"npe01__One2OneContact__r","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates whether or not this Account is special for Contacts (Household, One-to-One, Individual) vs a normal Account.","label":"_SYSTEM: IsIndividual","length":0,"mask":null,"maskType":null,"name":"npe01__SYSTEMIsIndividual__c","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":300,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates which Account Model this account is for: Household Account, One-to-One, or Individual (bucket).","label":"_SYSTEM: AccountType","length":100,"mask":null,"maskType":null,"name":"npe01__SYSTEM_AccountType__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Average won Opportunity amount related to this Account. Updated automatically.","label":"Average Gift","length":0,"mask":null,"maskType":null,"name":"npo02__AverageAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The sum of won Opportunity amounts in the Best Gift Year for this Account.","label":"Best Gift Year Total","length":0,"mask":null,"maskType":null,"name":"npo02__Best_Gift_Year_Total__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":12,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The year in which the largest total amount of won Opportunities related to this Account occurred. See also Best Gift Year Total.","label":"Best Gift Year","length":4,"mask":null,"maskType":null,"name":"npo02__Best_Gift_Year__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The earliest won Opportunity (by Close Date) with amount greater than zero related to this Account.","label":"First Gift Date","length":0,"mask":null,"maskType":null,"name":"npo02__FirstCloseDate__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"For Household Accounts, uses fields of non-excluded, ordered related Contacts. Generated automatically.","label":"Formal Greeting","length":255,"mask":null,"maskType":null,"name":"npo02__Formal_Greeting__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Household Phone","length":40,"mask":null,"maskType":null,"name":"npo02__HouseholdPhone__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"For Household Accounts, uses fields of non-excluded, ordered related Contacts. Generated automatically.","label":"Informal Greeting","length":255,"mask":null,"maskType":null,"name":"npo02__Informal_Greeting__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The largest amount of a won Opportunity related to this Account.","label":"Largest Gift","length":0,"mask":null,"maskType":null,"name":"npo02__LargestAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The most recent Close Date of a won Opportunity related to this Account with amount greater than zero.","label":"Last Gift Date","length":0,"mask":null,"maskType":null,"name":"npo02__LastCloseDate__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of the most recent (by Close Date) won Membership Opportunity (by Record Type) for this Account.","label":"Last Membership Amount","length":0,"mask":null,"maskType":null,"name":"npo02__LastMembershipAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The most recent Close Date of a won Membership Opportunity (by Record Type) related to this Account.","label":"Last Membership Date","length":0,"mask":null,"maskType":null,"name":"npo02__LastMembershipDate__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Membership level from most recent (by Close Date) won Membership Opportunity (by Record Type) related to this Account.\"","label":"Last Membership Level","length":255,"mask":null,"maskType":null,"name":"npo02__LastMembershipLevel__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Membership \"origin\" (new, renew...) from most recent (by Close Date) won Membership Opportunity (by Record Type) related to this Account.","label":"Last Membership Origin","length":255,"mask":null,"maskType":null,"name":"npo02__LastMembershipOrigin__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of the most recent won Opportunity (by Close Date) with amount greater than zero related to this Account.","label":"Last Gift Amount","length":0,"mask":null,"maskType":null,"name":"npo02__LastOppAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Membership end date from most recent (by Close Date) won Membership Opportunity (by Record Type) related to this Account.","label":"Membership End Date","length":0,"mask":null,"maskType":null,"name":"npo02__MembershipEndDate__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Membership start date from most recent (by Close Date) won Membership Opportunity (by Record Type) related to this Account.","label":"Membership Join Date","length":0,"mask":null,"maskType":null,"name":"npo02__MembershipJoinDate__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities related to this Account.","label":"Total Number of Gifts","length":0,"mask":null,"maskType":null,"name":"npo02__NumberOfClosedOpps__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Membership Opportunities (Record Type) related to this Account.","label":"Number of Memberships","length":0,"mask":null,"maskType":null,"name":"npo02__NumberOfMembershipOpps__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Account, closed within the year or fiscal year preceding last year (see Household Settings).","label":"Total Gifts Two Years Ago","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmount2YearsAgo__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Account, closed in the last number of days defined in Household Settings.","label":"Total Gifts Last N Days","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountLastNDays__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Account, closed in the current year or fiscal year.","label":"Total Gifts Last Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountLastYear__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Account, closed in the current year or fiscal year.","label":"Total Gifts This Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountThisYear__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Account, closed within the year or fiscal year preceding last year.","label":"Number of Gifts Two Years Ago","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosed2YearsAgo__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Account, closed in the last number of days defined in Household Settings.","label":"Number of Gifts Last N Days","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosedLastNDays__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Account, closed in the previous year or fiscal year (see Household Settings).","label":"Number of Gifts Last Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosedLastYear__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Account, closed in the current year or fiscal year (see Household Settings).","label":"Number of Gifts This Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosedThisYear__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":4099,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"For Household Accounts, specifies which name related fields should not be automatically populated by the Householding code.","label":"_SYSTEM: CUSTOM NAMING","length":4099,"mask":null,"maskType":null,"name":"npo02__SYSTEM_CUSTOM_NAMING__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Name","validFor":null,"value":"Name"},{"active":true,"defaultValue":false,"label":"Formal_Greeting__c","validFor":null,"value":"Formal_Greeting__c"},{"active":true,"defaultValue":false,"label":"Informal_Greeting__c","validFor":null,"value":"Informal_Greeting__c"}],"polymorphicForeignKey":false,"precision":3,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":false,"type":"multipicklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The smallest non-zero amount of a won Opportunity related to this Account.","label":"Smallest Gift","length":0,"mask":null,"maskType":null,"name":"npo02__SmallestAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Membership Opportunities (Record Type) related to this Account.","label":"Total Membership Amount","length":0,"mask":null,"maskType":null,"name":"npo02__TotalMembershipOppAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The sum of amounts of all won Opportunities related to this Account.","label":"Total Gifts","length":0,"mask":null,"maskType":null,"name":"npo02__TotalOppAmount__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"If selected, all members of this household are deceased. Do not update this field manually. It's updated automatically by NPSP as a result of updates to the Deceased checkbox on household member Contacts.","label":"All Household Members Deceased","length":0,"mask":null,"maskType":null,"name":"npsp__All_Members_Deceased__c","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The batch this Account was created in.","label":"Batch","length":18,"mask":null,"maskType":null,"name":"npsp__Batch__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["npsp__Batch__c"],"relationshipName":"npsp__Batch__r","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"When checked, Customizable Rollups will always use 'Skew Mode' to rollup Hard Credit and Soft Credits for this record.","label":"Customizable Rollups: Force Skew Mode","length":0,"mask":null,"maskType":null,"name":"npsp__CustomizableRollups_UseSkewMode__c","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":4099,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The general program area(s) that this organization funds.","label":"Funding Focus","length":4099,"mask":null,"maskType":null,"name":"npsp__Funding_Focus__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Animal Welfare","validFor":null,"value":"Animal Welfare"},{"active":true,"defaultValue":false,"label":"Arts and Culture","validFor":null,"value":"Arts and Culture"},{"active":true,"defaultValue":false,"label":"Capacity Building","validFor":null,"value":"Capacity Building"},{"active":true,"defaultValue":false,"label":"Education","validFor":null,"value":"Education"},{"active":true,"defaultValue":false,"label":"Environmental","validFor":null,"value":"Environmental"},{"active":true,"defaultValue":false,"label":"Health Services","validFor":null,"value":"Health Services"},{"active":true,"defaultValue":false,"label":"Human Services","validFor":null,"value":"Human Services"}],"polymorphicForeignKey":false,"precision":4,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":false,"type":"multipicklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates that this organization is a grantmaking organization.","label":"Grantmaker","length":0,"mask":null,"maskType":null,"name":"npsp__Grantmaker__c","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The name of the company's Matching Gift administrator.","label":"Matching Gift Administrator Name","length":255,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Administrator_Name__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The maximum amount the company will match on any single gift.","label":"Matching Gift Amount Max","length":0,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Amount_Max__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The smallest amount the company will match on any single gift.","label":"Matching Gift Amount Min","length":0,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Amount_Min__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The maximum amount the company will match each year for an employee.","label":"Matching Gift Annual Employee Max","length":0,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Annual_Employee_Max__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":98304,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":"plaintextarea","filterable":false,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Additional notes about the company's Matching Gift program.","label":"Matching Gift Comments","length":32768,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Comments__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":false,"type":"textarea","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates that this company offers Matching Gifts.","label":"Matching Gift Company","length":0,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Company__c","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Email of the company's Matching Gift administrator.","label":"Matching Gift Email","length":80,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Email__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"email","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The date you last updated the Matching Gift information.","label":"Matching Gift Info Updated","length":0,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Info_Updated__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The portion of a gift the company will match.","label":"Matching Gift Percent","length":0,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Percent__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":5,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"percent","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The phone number of the company's Matching Gift administrator.","label":"Matching Gift Phone","length":40,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Phone__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Use this field to note any age restrictions this company places on Matching Gift eligibility.","label":"Matching Gift Request Deadline","length":255,"mask":null,"maskType":null,"name":"npsp__Matching_Gift_Request_Deadline__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":true,"calculatedFormula":"year(npo02__MembershipEndDate__c ) - year(npo02__MembershipJoinDate__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of years that a member of this Household has had a Membership (read only).","label":"Membership Span","length":0,"mask":null,"maskType":null,"name":"npsp__Membership_Span__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"if ( npo02__MembershipEndDate__c ( TODAY() -\nIF(NOT(ISNULL($Setup.npo02__Households_Settings__c.npo02__Membership_Grace_Period__c)), $Setup.npo02__Households_Settings__c.npo02__Membership_Grace_Period__c, 30)) , \"Grace Period\" , \"Expired\") , if(isnull(npo02__MembershipEndDate__c ),\"\",\"Current\"))","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The Membership status of this Household, for example, Current, Expired, or Grace Period. The value is based on Membership End Date and Grace Period. The Default Grace Period is set in NPSP Settings, under Household Settings. This field is read only.","label":"Membership Status","length":1300,"mask":null,"maskType":null,"name":"npsp__Membership_Status__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of Contacts who are members of this Household.","label":"Number of Household Members","length":0,"mask":null,"maskType":null,"name":"npsp__Number_of_Household_Members__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":10,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates if this Account is an active, lapsed, or former recurring donor. The value is based on the Status of the related Recurring Donations.","label":"Sustainer","length":255,"mask":null,"maskType":null,"name":"npsp__Sustainer__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Active","validFor":null,"value":"Active"},{"active":true,"defaultValue":false,"label":"Lapsed","validFor":null,"value":"Lapsed"},{"active":true,"defaultValue":false,"label":"Former","validFor":null,"value":"Former"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Undeliverable Billing Address","length":0,"mask":null,"maskType":null,"name":"npsp__Undeliverable_Address__c","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Level","length":18,"mask":null,"maskType":null,"name":"Level__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["npsp__Level__c"],"relationshipName":"Level__r","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Previous Level","length":18,"mask":null,"maskType":null,"name":"Previous_Level__c","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["npsp__Level__c"],"relationshipName":"Previous_Level__r","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":true,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Level","length":255,"mask":null,"maskType":null,"name":"Level__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Secondary","validFor":null,"value":"Secondary"},{"active":true,"defaultValue":false,"label":"Tertiary","validFor":null,"value":"Tertiary"},{"active":true,"defaultValue":false,"label":"Primary","validFor":null,"value":"Primary"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":300,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Languages","length":100,"mask":null,"maskType":null,"name":"Languages__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Alternate Email is an optional fourth email: Not personal, preferred, or work email.","label":"Alternate Email","length":80,"mask":null,"maskType":null,"name":"npe01__AlternateEmail__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"email","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"See Preferred Email field.","label":"Personal Email","length":80,"mask":null,"maskType":null,"name":"npe01__HomeEmail__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"email","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":"Home","defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Which phone number should be used for most communications involving this Contact?","label":"Preferred Phone","length":255,"mask":null,"maskType":null,"name":"npe01__PreferredPhone__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":true,"label":"Home","validFor":null,"value":"Home"},{"active":true,"defaultValue":false,"label":"Work","validFor":null,"value":"Work"},{"active":true,"defaultValue":false,"label":"Mobile","validFor":null,"value":"Mobile"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":"Personal","defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Which email should be used for most communications involving this Contact?","label":"Preferred Email","length":255,"mask":null,"maskType":null,"name":"npe01__Preferred_Email__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":true,"label":"Personal","validFor":null,"value":"Personal"},{"active":true,"defaultValue":false,"label":"Work","validFor":null,"value":"Work"},{"active":true,"defaultValue":false,"label":"Alternate","validFor":null,"value":"Alternate"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Which type of address is the mailing address?","label":"Primary Address Type","length":255,"mask":null,"maskType":null,"name":"npe01__Primary_Address_Type__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Home","validFor":null,"value":"Home"},{"active":true,"defaultValue":false,"label":"Work","validFor":null,"value":"Work"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Excludes this Contact from any Individual Account processing. If you leave the Account field blank, it will remain blank.","label":"Private","length":0,"mask":null,"maskType":null,"name":"npe01__Private__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"What type of address is the other address?","label":"Secondary Address Type","length":255,"mask":null,"maskType":null,"name":"npe01__Secondary_Address_Type__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Home","validFor":null,"value":"Home"},{"active":true,"defaultValue":false,"label":"Work","validFor":null,"value":"Work"},{"active":true,"defaultValue":false,"label":"Other","validFor":null,"value":"Other"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":"One-to-One","defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"NO LONGER USED - Indicates which model drives Account relationship behavior: Individual (Bucket) or One-to-One","label":"DEPRECATED - _SYSTEM: ACCOUNT PROCESSOR","length":255,"mask":null,"maskType":null,"name":"npe01__SystemAccountProcessor__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":true,"label":"One-to-One","validFor":null,"value":"One-to-One"},{"active":true,"defaultValue":false,"label":"Individual","validFor":null,"value":"Individual"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":240,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"See Preferred Email field.","label":"Work Email","length":80,"mask":null,"maskType":null,"name":"npe01__WorkEmail__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"email","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":120,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"See 'Preferred Phone' field.","label":"Work Phone","length":40,"mask":null,"maskType":null,"name":"npe01__WorkPhone__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"phone","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"IF(\n ISPICKVAL(npe01__Primary_Address_Type__pc,\"Home\"),\n IF(ISBLANK(PersonMailingStreet), \"\", PersonMailingStreet & \", \") & \n IF(ISBLANK(PersonMailingCity), \"\", PersonMailingCity & \", \")& \n IF(ISBLANK(PersonMailingState), \"\", PersonMailingState & \" \")& \n IF(ISBLANK(PersonMailingPostalCode), \"\", PersonMailingPostalCode) & \n IF(ISBLANK(PersonMailingCountry), \"\", \", \" &PersonMailingCountry)\n,\nIF(ISPICKVAL(npe01__Secondary_Address_Type__pc,\"Home\"),\n IF(ISBLANK(PersonOtherStreet), \"\", PersonOtherStreet & \", \") & \n IF(ISBLANK(PersonOtherCity), \"\", PersonOtherCity & \", \")& \n IF(ISBLANK(PersonOtherState), \"\", PersonOtherState & \" \")& \n IF(ISBLANK(PersonOtherPostalCode), \"\", PersonOtherPostalCode) & \n IF(ISBLANK(PersonOtherCountry), \"\", \", \" & PersonOtherCountry)\n ,\"\"\n)\n)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: Uses primary address type field to determine Home Address from Mailing or Other address.","label":"Home Address","length":1300,"mask":null,"maskType":null,"name":"npe01__Home_Address__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"NULLVALUE(IF( npe01__SYSTEM_AccountType__c==\"\", TEXT(Type),npe01__SYSTEM_AccountType__c),\"Organization\" )","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: In order of preference, the SYSTEM_AccountType field, the Account Type field, and \"Organization\".","label":"Organization Type","length":1300,"mask":null,"maskType":null,"name":"npe01__Organization_Type__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"IF(\n ISPICKVAL(npe01__Primary_Address_Type__pc,\"Other\"),\n IF(ISBLANK(PersonMailingStreet), \"\", PersonMailingStreet & \", \") & \n IF(ISBLANK(PersonMailingCity), \"\", PersonMailingCity & \", \")& \n IF(ISBLANK(PersonMailingState), \"\", PersonMailingState & \" \")& \n IF(ISBLANK(PersonMailingPostalCode), \"\", PersonMailingPostalCode) & \n IF(ISBLANK(PersonMailingCountry), \"\", \", \" &PersonMailingCountry)\n,\nIF(ISPICKVAL(npe01__Secondary_Address_Type__pc,\"Other\"),\n IF(ISBLANK(PersonOtherStreet), \"\", PersonOtherStreet & \", \") & \n IF(ISBLANK(PersonOtherCity), \"\", PersonOtherCity & \", \")& \n IF(ISBLANK(PersonOtherState), \"\", PersonOtherState & \" \")& \n IF(ISBLANK(PersonOtherPostalCode), \"\", PersonOtherPostalCode) & \n IF(ISBLANK(PersonOtherCountry), \"\", \", \" & PersonOtherCountry)\n ,\"\"\n)\n)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: If the Primary Address Type is Other, the Mailing Address. If the Secondary Address Type is Other, the Other Address.","label":"Other Address","length":1300,"mask":null,"maskType":null,"name":"npe01__Other_Address__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"IF(npe01__SYSTEMIsIndividual__c,\"Individual\",\"Organization\")","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: \"Individual\" or \"Organization\" depending on Account setting.","label":"Type of Account","length":1300,"mask":null,"maskType":null,"name":"npe01__Type_of_Account__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"IF(\n ISPICKVAL(npe01__Primary_Address_Type__pc,\"Work\"),\n IF(ISBLANK(PersonMailingStreet), \"\", PersonMailingStreet & \", \") & \n IF(ISBLANK(PersonMailingCity), \"\", PersonMailingCity & \", \")& \n IF(ISBLANK(PersonMailingState), \"\", PersonMailingState & \" \")& \n IF(ISBLANK(PersonMailingPostalCode), \"\", PersonMailingPostalCode) & \n IF(ISBLANK(PersonMailingCountry), \"\", \", \" &PersonMailingCountry)\n,\nIF(ISPICKVAL(npe01__Secondary_Address_Type__pc,\"Work\"),\n IF(ISBLANK(PersonOtherStreet), \"\", PersonOtherStreet & \", \") & \n IF(ISBLANK(PersonOtherCity), \"\", PersonOtherCity & \", \")& \n IF(ISBLANK(PersonOtherState), \"\", PersonOtherState & \" \")& \n IF(ISBLANK(PersonOtherPostalCode), \"\", PersonOtherPostalCode) & \n IF(ISBLANK(PersonOtherCountry), \"\", \", \" & PersonOtherCountry)\n ,\"\" \n)\n)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: If the Primary Address Type is Work, the Mailing Address. If the Secondary Address Type is Work, the Other Address.","label":"Work Address","length":1300,"mask":null,"maskType":null,"name":"npe01__Work_Address__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The average amount of all won Opportunities related to this Contact by a primary Opportunity Contact Role.","label":"Average Gift","length":0,"mask":null,"maskType":null,"name":"npo02__AverageAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The sum of won Opportunity amounts in the Best Gift Year.","label":"Best Gift Year Total","length":0,"mask":null,"maskType":null,"name":"npo02__Best_Gift_Year_Total__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":12,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The year of the largest total amount of won Opportunities related to this Contact by primary Contact Role. See also Best Gift Year Total.","label":"Best Gift Year","length":4,"mask":null,"maskType":null,"name":"npo02__Best_Gift_Year__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The oldest Close Date of a won Opportunity with amount greater than zero related to this Contact by primary Opportunity Contact Role.","label":"First Gift Date","length":0,"mask":null,"maskType":null,"name":"npo02__FirstCloseDate__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Determines the order in which Contact names are used to generate Household name.","label":"Household Naming Order","length":0,"mask":null,"maskType":null,"name":"npo02__Household_Naming_Order__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Household","length":18,"mask":null,"maskType":null,"name":"npo02__Household__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["npo02__Household__c"],"relationshipName":"npo02__Household__pr","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The largest amount of a won Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Largest Gift","length":0,"mask":null,"maskType":null,"name":"npo02__LargestAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The close date of the most recent won Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Last Gift Date","length":0,"mask":null,"maskType":null,"name":"npo02__LastCloseDate__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of the most recent (by Close Date) won Membership (Record Type) Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Last Membership Amount","length":0,"mask":null,"maskType":null,"name":"npo02__LastMembershipAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The most recent Close Date of a won Membership (Record Type) Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Last Membership Date","length":0,"mask":null,"maskType":null,"name":"npo02__LastMembershipDate__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The level of the most recent (by Close Date) won Membership (Record Type) Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Last Membership Level","length":255,"mask":null,"maskType":null,"name":"npo02__LastMembershipLevel__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The origin (new, renewal, reacquire...) of the most recent (Close Date) won Membership (Record Type) Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Last Membership Origin","length":255,"mask":null,"maskType":null,"name":"npo02__LastMembershipOrigin__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of the most recent won Opportunity by Close Date related to this Contact by primary Opportunity Contact Role.","label":"Last Gift Amount","length":0,"mask":null,"maskType":null,"name":"npo02__LastOppAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The Membership End Date of the most recent (Close Date) Membership Opportunity (Record Type) related to this Contact by primary Opportunity Contact Role.","label":"Membership End Date","length":0,"mask":null,"maskType":null,"name":"npo02__MembershipEndDate__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The Join Date of the oldest (Close Date) Membership Opportunity (Record Type) related to this Contact by primary Opportunity Contact Role.","label":"Membership Join Date","length":0,"mask":null,"maskType":null,"name":"npo02__MembershipJoinDate__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":4099,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Select to exclude this Contact's name from various generated Household names.","label":"Naming Exclusions","length":4099,"mask":null,"maskType":null,"name":"npo02__Naming_Exclusions__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Household__c.Name","validFor":null,"value":"Household__c.Name"},{"active":true,"defaultValue":false,"label":"Household__c.Formal_Greeting__c","validFor":null,"value":"Household__c.Formal_Greeting__c"},{"active":true,"defaultValue":false,"label":"Household__c.Informal_Greeting__c","validFor":null,"value":"Household__c.Informal_Greeting__c"}],"polymorphicForeignKey":false,"precision":3,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":false,"type":"multipicklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Contact by primary Opportunity Contact Role.","label":"Total Number of Gifts","length":0,"mask":null,"maskType":null,"name":"npo02__NumberOfClosedOpps__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of won Membership Opportunities (Record Type) related to this Contact by primary Opportunity Contact Role.","label":"Number of Memberships","length":0,"mask":null,"maskType":null,"name":"npo02__NumberOfMembershipOpps__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Contact by primary Opportunity Contact Role, closed two years ago.","label":"Total Gifts Two Years Ago","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmount2YearsAgo__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Contact by primary Opportunity Contact Role, closed in the last number of days defined in Household Settings.","label":"Total Gifts Last N Days","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountLastNDays__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Contact by primary Opportunity Contact Role, closed last year.","label":"Total Gifts Last Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountLastYear__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Opportunities related to this Contact by primary Opportunity Contact Role, closed this year.","label":"Total Gifts This Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountThisYear__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Contact by primary Opportunity Contact Role, closed two years ago.","label":"Number of Gifts Two Years Ago","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosed2YearsAgo__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Contact by primary Opportunity Contact Role, closed in the last number of days defined in Household Settings.","label":"Number of Gifts Last N Days","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosedLastNDays__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Contact by primary Opportunity Contact Role, closed last year.","label":"Number of Gifts Last Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosedLastYear__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of won Opportunities with amount greater than zero related to this Contact by primary Opportunity Contact Role, closed this year.","label":"Number of Gifts This Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppsClosedThisYear__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The smallest amount of a won Opportunity related to this Contact by primary Opportunity Contact Role.","label":"Smallest Gift","length":0,"mask":null,"maskType":null,"name":"npo02__SmallestAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The sum of soft credit Opportunity amounts last year determined by Opportunity Contact Role and filtered by Roles in Household Settings.","label":"Soft Credit Last Year","length":0,"mask":null,"maskType":null,"name":"npo02__Soft_Credit_Last_Year__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Total soft credit amount this year","label":"Soft Credit This Year","length":0,"mask":null,"maskType":null,"name":"npo02__Soft_Credit_This_Year__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Total amount in soft credits for this Contact's lifetime. Defined in the Household Settings tab.","label":"Soft Credit Total","length":0,"mask":null,"maskType":null,"name":"npo02__Soft_Credit_Total__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The sum of soft credit Opportunity amounts two years ago determined by Opportunity Contact Role and filtered by Roles in Household Settings.","label":"Soft Credit Two Years Ago","length":0,"mask":null,"maskType":null,"name":"npo02__Soft_Credit_Two_Years_Ago__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"_SYSTEM: HOUSEHOLD PROCESSOR-DEPRECATED","length":255,"mask":null,"maskType":null,"name":"npo02__SystemHouseholdProcessor__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"All Individual Contacts","validFor":null,"value":"All Individual Contacts"},{"active":true,"defaultValue":false,"label":"All New or Edited Contacts","validFor":null,"value":"All New or Edited Contacts"},{"active":true,"defaultValue":false,"label":"No Contacts","validFor":null,"value":"No Contacts"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of won Membership Opportunities (Record Type) related to this Contact by primary Opportunity Contact Role.","label":"Total Membership Amount","length":0,"mask":null,"maskType":null,"name":"npo02__TotalMembershipOppAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The sum of won Opportunity Amounts related to this Contact by primary Opportunity Contact Role.","label":"Total Gifts","length":0,"mask":null,"maskType":null,"name":"npo02__TotalOppAmount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"IF(npe01__Organization_Type__pc == 'Household Account',\n\nBillingStreet & BR() &\nBillingCity & IF(ISBLANK(BillingCity), \"\", \", \") & BillingState & \" \" & BillingPostalCode \n& IF(ISBLANK(BillingCountry), \"\", BR()& BillingCountry),\n\nnpo02__Household__pr.npo02__Formula_MailingAddress__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: Clone of the Household Mailing address (also a Formula)","label":"Household Mailing Address","length":1300,"mask":null,"maskType":null,"name":"npo02__Formula_HouseholdMailingAddress__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"IF(npe01__Organization_Type__pc == 'Household Account', \nnpo02__HouseholdPhone__c,\nnpo02__Household__pr.npo02__HouseholdPhone__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: Simple formula of Household Phone field.","label":"Household Phone","length":1300,"mask":null,"maskType":null,"name":"npo02__Formula_HouseholdPhone__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":true,"calculatedFormula":"IF(npe01__Organization_Type__pc == 'Household Account', \nnpo02__LastCloseDate__c,\nnpo02__Household__pr.npo02__LastCloseDate__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Last Household Gift Date","length":0,"mask":null,"maskType":null,"name":"npo02__LastCloseDateHH__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":true,"calculatedFormula":"IF(npe01__Organization_Type__pc == 'Household Account', \nnpo02__OppAmountLastYear__c,\nnpo02__Household__pr.npo02__OppAmountLastYear__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: Total Gifts Last Year on related Household.","label":"Total Household Gifts Last Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountLastYearHH__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":true,"calculatedFormula":"IF(npe01__Organization_Type__pc == 'Household Account', \nnpo02__OppAmountThisYear__c,\nnpo02__Household__pr.npo02__OppAmountThisYear__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: Total Gifts This Year on related Household.","label":"Total Household Gifts This Year","length":0,"mask":null,"maskType":null,"name":"npo02__OppAmountThisYearHH__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":true,"calculatedFormula":"IF(npe01__Organization_Type__pc == 'Household Account', \nnpo02__TotalOppAmount__c,\nnpo02__Household__pr.npo02__TotalOppAmount__c)","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Formula: Total Gifts on related Household.","label":"Total Household Gifts","length":0,"mask":null,"maskType":null,"name":"npo02__Total_Household_Gifts__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Lookup to the Batch that this Contact was created in.","label":"Batch","length":18,"mask":null,"maskType":null,"name":"npsp__Batch__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["npsp__Batch__c"],"relationshipName":"npsp__Batch__pr","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Lookup to the current Address record for this Contact. NPSP populates the Contact's Mailing Address field with the street, city, state, and postal code from the current Address record.","label":"Current Address","length":18,"mask":null,"maskType":null,"name":"npsp__Current_Address__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["npsp__Address__c"],"relationshipName":"npsp__Current_Address__pr","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"When checked, Customizable Rollups will always use 'Skew Mode' to rollup Hard Credit and Soft Credits for this record.","label":"Customizable Rollups: Force Skew Mode","length":0,"mask":null,"maskType":null,"name":"npsp__CustomizableRollups_UseSkewMode__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates the Contact is deceased. When selected, NPSP excludes this Contact from Household Naming (unless this Contact is the only one in the Household) and from standard email and phone lists.","label":"Deceased","length":0,"mask":null,"maskType":null,"name":"npsp__Deceased__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates the Contact does not want your organization to call or email. Use this field to filter out Contacts in email and phone lists. When selected, NPSP excludes this Contact from Opportunity Acknowledgements.","label":"Do Not Contact","length":0,"mask":null,"maskType":null,"name":"npsp__Do_Not_Contact__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"When selected, NPSP excludes this Contact from the Household Formal Greeting (set in NPSP Settings | People | Households).","label":"Exclude from Household Formal Greeting","length":0,"mask":null,"maskType":null,"name":"npsp__Exclude_from_Household_Formal_Greeting__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"When selected, NPSP excludes this Contact from the Household Informal Greeting (set in NPSP Settings | People | Households).","label":"Exclude from Household Informal Greeting","length":0,"mask":null,"maskType":null,"name":"npsp__Exclude_from_Household_Informal_Greeting__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"When selected, NPSP excludes this Contact from the Household Name (set in NPSP Settings | People | Households).","label":"Exclude from Household Name","length":0,"mask":null,"maskType":null,"name":"npsp__Exclude_from_Household_Name__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of this Contact's earliest soft credit.","label":"First Soft Credit Amount","length":0,"mask":null,"maskType":null,"name":"npsp__First_Soft_Credit_Amount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The date of this Contact's earliest soft credit.","label":"First Soft Credit Date","length":0,"mask":null,"maskType":null,"name":"npsp__First_Soft_Credit_Date__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"if(npe01__SYSTEM_AccountType__c=='Household Account',CASESAFEID(Account),CASESAFEID(npo02__Household__pc))","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Holds the Id of the Household record (either Household Account, or Household object) this Contact is currently associated with (read only).","label":"HHId","length":1300,"mask":null,"maskType":null,"name":"npsp__HHId__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of this Contact's largest soft credit.","label":"Largest Soft Credit Amount","length":0,"mask":null,"maskType":null,"name":"npsp__Largest_Soft_Credit_Amount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The date of this Contact's largest soft credit.","label":"Largest Soft Credit Date","length":0,"mask":null,"maskType":null,"name":"npsp__Largest_Soft_Credit_Date__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The amount of this Contact's most recent soft credit.","label":"Last Soft Credit Amount","length":0,"mask":null,"maskType":null,"name":"npsp__Last_Soft_Credit_Amount__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The date of this Contact's most recent soft credit.","label":"Last Soft Credit Date","length":0,"mask":null,"maskType":null,"name":"npsp__Last_Soft_Credit_Date__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:date","sortable":true,"type":"date","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of soft credits attributed to this Contact that closed in the last N days. The value of N is set in NPSP Settings | Donations | Donor Statistics.","label":"Number of Soft Credits Last N Days","length":0,"mask":null,"maskType":null,"name":"npsp__Number_of_Soft_Credits_Last_N_Days__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of soft credits attributed to this Contact in the previous calendar year.","label":"Number of Soft Credits Last Year","length":0,"mask":null,"maskType":null,"name":"npsp__Number_of_Soft_Credits_Last_Year__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of soft credits attributed to this Contact this calendar year.","label":"Number of Soft Credits This Year","length":0,"mask":null,"maskType":null,"name":"npsp__Number_of_Soft_Credits_This_Year__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The number of soft credits attributed to this Contact two calendar years ago.","label":"Number of Soft Credits Two Years Ago","length":0,"mask":null,"maskType":null,"name":"npsp__Number_of_Soft_Credits_Two_Years_Ago__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total number of soft credits attributed to this Contact.","label":"Number of Soft Credits","length":0,"mask":null,"maskType":null,"name":"npsp__Number_of_Soft_Credits__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":18,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"double","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":18,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The Account marked as Primary in this Contact's list of Organization Affiliations.","label":"Primary Affiliation","length":18,"mask":null,"maskType":null,"name":"npsp__Primary_Affiliation__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":["Account"],"relationshipName":"npsp__Primary_Affiliation__pr","relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"tns:ID","sortable":true,"type":"reference","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":true,"calculatedFormula":"npe01__One2OneContact__c = Id","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates if this Contact is designated as the Primary Contact on their Account (read only).","label":"Primary Contact","length":0,"mask":null,"maskType":null,"name":"npsp__Primary_Contact__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":"0","defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The total amount of soft credit attributed to this Contact in the last N days. The value of N is set in NPSP Settings | Donations | Donor Statistics.","label":"Soft Credit Last N Days","length":0,"mask":null,"maskType":null,"name":"npsp__Soft_Credit_Last_N_Days__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":16,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":2,"searchPrefilterable":false,"soapType":"xsd:double","sortable":true,"type":"currency","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"Indicates if this Contact is an active, lapsed, or former recurring donor. The value is based on the Status of the related Recurring Donations.","label":"Sustainer","length":255,"mask":null,"maskType":null,"name":"npsp__Sustainer__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Active","validFor":null,"value":"Active"},{"active":true,"defaultValue":false,"label":"Lapsed","validFor":null,"value":"Lapsed"},{"active":true,"defaultValue":false,"label":"Former","validFor":null,"value":"Former"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Undeliverable Mailing Address","length":0,"mask":null,"maskType":null,"name":"npsp__Undeliverable_Address__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":false,"aiPredictionField":false,"autoNumber":false,"byteLength":0,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":false,"defaultValueFormula":null,"defaultedOnCreate":true,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"When selected, NPSP will not sync this Contact's address with the Household Default or Seasonal Address. Use this to maintain a separate Mailing Address for this particular Contact.","label":"Address Override","length":0,"mask":null,"maskType":null,"name":"npsp__is_Address_Override__pc","nameField":false,"namePointing":false,"nillable":false,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:boolean","sortable":true,"type":"boolean","unique":false,"updateable":true,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":3900,"calculated":true,"calculatedFormula":"npsp__Current_Address__pr.npsp__Verification_Status__c","cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":false,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":true,"groupable":false,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":"The verification status of the current Address. If this Address requires verification, navigate to the current Address record and select Verify Address. Consult NPSP documentation for more information on Address Verification.","label":"Address Verification Status","length":1300,"mask":null,"maskType":null,"name":"npsp__Address_Verification_Status__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"string","unique":false,"updateable":false,"writeRequiresMasterRead":false},{"aggregatable":true,"aiPredictionField":false,"autoNumber":false,"byteLength":765,"calculated":false,"calculatedFormula":null,"cascadeDelete":false,"caseSensitive":false,"compoundFieldName":null,"controllerName":null,"createable":true,"custom":true,"defaultValue":null,"defaultValueFormula":null,"defaultedOnCreate":false,"dependentPicklist":false,"deprecatedAndHidden":false,"digits":0,"displayLocationInDecimal":false,"encrypted":false,"externalId":false,"extraTypeInfo":null,"filterable":true,"filteredLookupInfo":null,"formulaTreatNullNumberAsZero":false,"groupable":true,"highScaleNumber":false,"htmlFormatted":false,"idLookup":false,"inlineHelpText":null,"label":"Gender","length":255,"mask":null,"maskType":null,"name":"Gender__pc","nameField":false,"namePointing":false,"nillable":true,"permissionable":true,"picklistValues":[{"active":true,"defaultValue":false,"label":"Male","validFor":null,"value":"Male"},{"active":true,"defaultValue":false,"label":"Female","validFor":null,"value":"Female"},{"active":true,"defaultValue":false,"label":"Non-Binary","validFor":null,"value":"Non-Binary"},{"active":true,"defaultValue":false,"label":"Prefer Not To Say","validFor":null,"value":"Prefer Not To Say"}],"polymorphicForeignKey":false,"precision":0,"queryByDistance":false,"referenceTargetField":null,"referenceTo":[],"relationshipName":null,"relationshipOrder":null,"restrictedDelete":false,"restrictedPicklist":false,"scale":0,"searchPrefilterable":false,"soapType":"xsd:string","sortable":true,"type":"picklist","unique":false,"updateable":true,"writeRequiresMasterRead":false}],"hasSubtypes":false,"implementedBy":null,"implementsInterfaces":null,"isInterface":false,"isSubtype":false,"keyPrefix":"001","label":"Account","labelPlural":"Accounts","layoutable":true,"listviewable":null,"lookupLayoutable":null,"mergeable":true,"mruEnabled":true,"name":"Account","namedLayoutInfos":[],"networkScopeFieldName":null,"queryable":true,"recordTypeInfos":[{"active":true,"available":true,"defaultRecordTypeMapping":false,"developerName":"HH_Account","master":false,"name":"Household Account","recordTypeId":"0125j000000RqVkAAK","urls":{"layout":"/services/data/v59.0/sobjects/Account/describe/layouts/0125j000000RqVkAAK"}},{"active":true,"available":true,"defaultRecordTypeMapping":true,"developerName":"Organization","master":false,"name":"Organization","recordTypeId":"0125j000000RqVlAAK","urls":{"layout":"/services/data/v59.0/sobjects/Account/describe/layouts/0125j000000RqVlAAK"}},{"active":true,"available":true,"defaultRecordTypeMapping":false,"developerName":"PersonAccount","master":false,"name":"Person Account","recordTypeId":"0125j000000bo4yAAA","urls":{"layout":"/services/data/v59.0/sobjects/Account/describe/layouts/0125j000000bo4yAAA"}},{"active":true,"available":true,"defaultRecordTypeMapping":false,"developerName":"PersonAccount","master":false,"name":"PersonAccount","recordTypeId":"0125j000000bo53AAA","urls":{"layout":"/services/data/v59.0/sobjects/Account/describe/layouts/0125j000000bo53AAA"}},{"active":true,"available":true,"defaultRecordTypeMapping":false,"developerName":"Master","master":true,"name":"Master","recordTypeId":"012000000000000AAA","urls":{"layout":"/services/data/v59.0/sobjects/Account/describe/layouts/012000000000000AAA"}}],"replicateable":true,"retrieveable":true,"searchLayoutable":true,"searchable":true,"sobjectDescribeOption":"FULL","supportedScopes":[{"label":"All accounts","name":"everything"},{"label":"My accounts","name":"mine"},{"label":"Filter by scope","name":"scopingRule"},{"label":"My team's accounts","name":"team"}],"triggerable":true,"undeletable":true,"updateable":true,"urls":{"compactLayouts":"/services/data/v59.0/sobjects/Account/describe/compactLayouts","rowTemplate":"/services/data/v59.0/sobjects/Account/{ID}","approvalLayouts":"/services/data/v59.0/sobjects/Account/describe/approvalLayouts","uiDetailTemplate":"https://orgname.my.salesforce.com/{ID}","uiEditTemplate":"https://orgname.my.salesforce.com/{ID}/e","listviews":"/services/data/v59.0/sobjects/Account/listviews","describe":"/services/data/v59.0/sobjects/Account/describe","uiNewRecord":"https://orgname.my.salesforce.com/001/e","quickActions":"/services/data/v59.0/sobjects/Account/quickActions","layouts":"/services/data/v59.0/sobjects/Account/describe/layouts","sobject":"/services/data/v59.0/sobjects/Account"}} diff --git a/datasets/upsert/upsert_mapping_recordtypes.yml b/datasets/upsert/upsert_mapping_recordtypes.yml new file mode 100644 index 0000000000..2f47f488af --- /dev/null +++ b/datasets/upsert/upsert_mapping_recordtypes.yml @@ -0,0 +1,8 @@ +Upsert Accounts: + api: rest + sf_object: Account + action: etl_upsert + update_key: AccountNumber + fields: + - AccountNumber + - RecordTypeId diff --git a/datasets/upsert/upsert_recordtypes.sql b/datasets/upsert/upsert_recordtypes.sql new file mode 100644 index 0000000000..7b2308ee5f --- /dev/null +++ b/datasets/upsert/upsert_recordtypes.sql @@ -0,0 +1,23 @@ +BEGIN TRANSACTION; +CREATE TABLE "Account" ( + id INTEGER NOT NULL, + "Name" VARCHAR(255), + "AccountNumber" VARCHAR(255), + "RecordTypeId" VARCHAR(255), + "IsPersonAccount" VARCHAR(255), + PRIMARY KEY (id) +); +INSERT INTO "Account" VALUES(1,'Sitwell-Bluth', "12345", '0125j000000bo4yAAA','true'); +INSERT INTO "Account" VALUES(2,'John-Doe', "456789", '0125j000000bo53AAA','false'); +INSERT INTO "Account" VALUES(3,'Jane-Doe', "422", '0125j000000bo53AAA','false'); +CREATE TABLE "Account_rt_mapping" ( + record_type_id VARCHAR(18) NOT NULL, + developer_name VARCHAR(255), + is_person_type BOOLEAN, + PRIMARY KEY (record_type_id) +); +INSERT INTO "Account_rt_mapping" VALUES('0125j000000RqVkAAK','HH_Account',0); +INSERT INTO "Account_rt_mapping" VALUES('0125j000000RqVlAAK','Organization',0); +INSERT INTO "Account_rt_mapping" VALUES('0125j000000bo4yAAA','PersonAccount',1); +INSERT INTO "Account_rt_mapping" VALUES('0125j000000bo53AAA','PersonAccount',0); +COMMIT; \ No newline at end of file From a627caa0718250780ad6690b54e0c0aa230233a8 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:04:45 +0530 Subject: [PATCH 95/98] Rollback Operation for Inserts and Upserts (#3718) [W-14522337](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001et8KzYAI/view) and [W-14522327](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001etNspYAE/view) The implementation is as follows: 1. For inserts, we maintain a separate table `{sobject}_insert_rollback` for records that have been created (either through insert or upsert) where we enter only the `sf_id` 2. For upserts, before we load the records, we store the previous values of the records that will get updated (not upserted) in a table called `{sobject}_upsert_rollback`. It will have all the fields that are given in the `mapping.yml` file along with the `sf_id`. 3. When rollback occurs, it is as follows, in reverse order of tables created (and only considering the rollback tables): a. For insert, we delete all the `sf_id` b. For upsert, we upsert again the previous values --------- Co-authored-by: Naman Jain Co-authored-by: Jaipal Reddy Kasturi --- cumulusci/tasks/bulkdata/load.py | 199 ++++++++++- cumulusci/tasks/bulkdata/step.py | 106 +++++- cumulusci/tasks/bulkdata/tests/test_load.py | 315 ++++++++++++++++-- cumulusci/tasks/bulkdata/tests/test_step.py | 117 ++++++- cumulusci/tasks/bulkdata/tests/test_upsert.py | 10 + cumulusci/tasks/bulkdata/tests/utils.py | 3 + 6 files changed, 707 insertions(+), 43 deletions(-) diff --git a/cumulusci/tasks/bulkdata/load.py b/cumulusci/tasks/bulkdata/load.py index 9a4f82ecf8..9fa66c2a4f 100644 --- a/cumulusci/tasks/bulkdata/load.py +++ b/cumulusci/tasks/bulkdata/load.py @@ -9,6 +9,7 @@ from sqlalchemy.ext.automap import automap_base from sqlalchemy.orm import Session +from cumulusci.core.enums import StrEnum from cumulusci.core.exceptions import BulkDataException, TaskOptionsError from cumulusci.core.utils import process_bool_arg from cumulusci.salesforce_api.org_schema import get_org_schema @@ -28,9 +29,11 @@ ) from cumulusci.tasks.bulkdata.step import ( DEFAULT_BULK_BATCH_SIZE, + DataApi, DataOperationJobResult, DataOperationStatus, DataOperationType, + RestApiDmlOperation, get_dml_operation, ) from cumulusci.tasks.bulkdata.upsert_utils import ( @@ -88,6 +91,9 @@ class LoadData(SqlAlchemyMixin, BaseSalesforceApiTask): "org_shape_match_only": { "description": "When True, all path options are ignored and only a dataset matching the org shape name will be loaded. Defaults to False." }, + "enable_rollback": { + "description": "When True, performs rollback operation incase of error. Defaults to False" + }, } row_warning_limit = 10 @@ -115,6 +121,9 @@ def _init_options(self, kwargs): self.options["set_recently_viewed"] = process_bool_arg( self.options.get("set_recently_viewed", True) ) + self.options["enable_rollback"] = process_bool_arg( + self.options.get("enable_rollback", False) + ) def _init_dataset(self): """Find the dataset paths to use with the following sequence: @@ -261,13 +270,33 @@ def _execute_step( step, query = self.configure_step(mapping) with tempfile.TemporaryFile(mode="w+t") as local_ids: + # Store the previous values of the records before upsert + # This is so that we can perform rollback + if ( + mapping.action + in [ + DataOperationType.ETL_UPSERT, + DataOperationType.UPSERT, + DataOperationType.UPDATE, + ] + and self.options["enable_rollback"] + ): + UpdateRollback.prepare_for_rollback( + self, step, self._stream_queried_data(mapping, local_ids, query) + ) step.start() step.load_records(self._stream_queried_data(mapping, local_ids, query)) step.end() + # Process Job Results if step.job_result.status is not DataOperationStatus.JOB_FAILURE: local_ids.seek(0) self._process_job_results(mapping, step, local_ids) + elif ( + step.job_result.status is DataOperationStatus.JOB_FAILURE + and self.options["enable_rollback"] + ): + Rollback._perform_rollback(self) return step.job_result @@ -454,7 +483,7 @@ def _process_job_results(self, mapping, step, local_ids): id_table_name = self._initialize_id_table(mapping, self.reset_oids) conn = self.session.connection() - results_generator = self._generate_results_id_map(step, local_ids) + sf_id_results = self._generate_results_id_map(step, local_ids) # If we know we have no successful inserts, don't attempt to persist Ids. # Do, however, drain the generator to get error-checking behavior. @@ -465,11 +494,8 @@ def _process_job_results(self, mapping, step, local_ids): connection=conn, table=self.metadata.tables[id_table_name], columns=("id", "sf_id"), - record_iterable=results_generator, + record_iterable=sf_id_results, ) - else: - for r in results_generator: - pass # Drain generator to validate results # Contact records for Person Accounts are inserted during an Account # sf_object step. Insert records into the Contact ID table for @@ -496,16 +522,37 @@ def _process_job_results(self, mapping, step, local_ids): def _generate_results_id_map(self, step, local_ids): """Consume results from load and prepare rows for id table. - Raise BulkDataException on row errors if configured to do so.""" + Raise BulkDataException on row errors if configured to do so. + Adds created records into insert_rollback Table + Performs rollback in case of any errors if enable_rollback is True""" error_checker = RowErrorChecker( self.logger, self.options["ignore_row_errors"], self.row_warning_limit ) local_ids = (lid.strip("\n") for lid in local_ids) + sf_id_results = [] + created_results = [] + failed_results = [] for result, local_id in zip(step.get_results(), local_ids): if result.success: - yield (local_id, result.id) + sf_id_results.append([local_id, result.id]) + if result.created: + created_results.append([result.id]) else: + failed_results.append([result, local_id]) + + # We record failed_results separately since if a unsuccesful record + # was in between, it would not store all the successful ids + for result, local_id in failed_results: + try: error_checker.check_for_row_error(result, local_id) + except Exception as e: + if self.options["enable_rollback"]: + CreateRollback.prepare_for_rollback(self, step, created_results) + Rollback._perform_rollback(self) + raise e + if self.options["enable_rollback"]: + CreateRollback.prepare_for_rollback(self, step, created_results) + return sf_id_results def _initialize_id_table(self, mapping, should_reset_table): """initalize or find table to hold the inserted SF Ids @@ -568,6 +615,9 @@ def _init_db(self): self.metadata.bind = connection self.inspector = inspect(parent_engine) + # empty the record of initalized tables + Rollback._initialized_rollback_tables_api = {} + # initialize the automap mapping self.base = automap_base(bind=connection, metadata=self.metadata) self.base.prepare(connection, reflect=True) @@ -810,6 +860,141 @@ def _set_viewed(self) -> T.List["SetRecentlyViewedInfo"]: return results +class RollbackType(StrEnum): + """Enum to specify type of rollback""" + + UPSERT = "upsert_rollback" + INSERT = "insert_rollback" + + +class Rollback: + # Store the table name and it's corresponding API (rest or bulk) + _initialized_rollback_tables_api = {} + + @staticmethod + def _create_tables_for_rollback(context, step, rollback_type: RollbackType) -> str: + """Create the tables required for upsert and insert rollback""" + table_name = f"{step.sobject}_{rollback_type}" + + if table_name not in Rollback._initialized_rollback_tables_api: + common_columns = [Column("Id", Unicode(255), primary_key=True)] + + additional_columns = ( + [Column(field, Unicode(255)) for field in step.fields if field != "Id"] + if rollback_type is RollbackType.UPSERT + else [] + ) + + columns = common_columns + additional_columns + + # Create the table + rollback_table = Table(table_name, context.metadata, *columns) + rollback_table.create() + + # Store the API in the initialized tables dictionary + if isinstance(step, RestApiDmlOperation): + Rollback._initialized_rollback_tables_api[table_name] = DataApi.REST + else: + Rollback._initialized_rollback_tables_api[table_name] = DataApi.BULK + + return table_name + + @staticmethod + def _perform_rollback(context): + """Perform total rollback""" + context.logger.info("--Initiated Rollback Procedure--") + for table in reversed(context.metadata.sorted_tables): + if table.name.endswith(RollbackType.INSERT): + CreateRollback._perform_rollback(context, table) + elif table.name.endswith(RollbackType.UPSERT): + UpdateRollback._perform_rollback(context, table) + context.logger.info("--Finished Rollback Procedure--") + + +class UpdateRollback: + @staticmethod + def prepare_for_rollback(context, step, records): + """Retrieve previous values for records being updated""" + results, columns = step.get_prev_record_values(records) + if results: + table_name = Rollback._create_tables_for_rollback( + context, step, RollbackType.UPSERT + ) + conn = context.session.connection() + sql_bulk_insert_from_records( + connection=conn, + table=context.metadata.tables[table_name], + columns=columns, + record_iterable=results, + ) + + @staticmethod + def _perform_rollback(context, table: Table) -> None: + """Perform rollback for updated records""" + sf_object = table.name.split(f"_{RollbackType.UPSERT.value}")[0] + records = context.session.query(table).all() + + if records: + context.logger.info(f"Reverting upserts for {sf_object}") + api_options = {"update_key": "Id"} + + # Use get_dml_operation to create an UPSERT step + step = get_dml_operation( + sobject=sf_object, + operation=DataOperationType.UPSERT, + api_options=api_options, + context=context, + fields=[column.name for column in table.columns], + api=Rollback._initialized_rollback_tables_api[table.name], + volume=len(records), + ) + step.start() + step.load_records(records) + step.end() + context.logger.info("Done") + + +class CreateRollback: + @staticmethod + def prepare_for_rollback(context, step, records): + """Store the sf_ids of all records that were created + to prepare for rollback""" + if records: + table_name = Rollback._create_tables_for_rollback( + context, step, RollbackType.INSERT + ) + conn = context.session.connection() + sql_bulk_insert_from_records( + connection=conn, + table=context.metadata.tables[table_name], + columns=["Id"], + record_iterable=records, + ) + + @staticmethod + def _perform_rollback(context, table: Table) -> None: + """Perform rollback for insert operation""" + sf_object = table.name.split(f"_{RollbackType.INSERT.value}")[0] + records = context.session.query(table).all() + + if records: + context.logger.info(f"Deleting {sf_object} records") + # Perform DELETE operation using get_dml_operation + step = get_dml_operation( + sobject=sf_object, + operation=DataOperationType.DELETE, + fields=["Id"], + api_options={}, + context=context, + api=Rollback._initialized_rollback_tables_api[table.name], + volume=len(records), + ) + step.start() + step.load_records(records) + step.end() + context.logger.info("Done") + + class StepResultInfo(T.NamedTuple): """Represent a Step Result in a form easily convertible to JSON""" diff --git a/cumulusci/tasks/bulkdata/step.py b/cumulusci/tasks/bulkdata/step.py index a5da05301a..edcb62afbb 100644 --- a/cumulusci/tasks/bulkdata/step.py +++ b/cumulusci/tasks/bulkdata/step.py @@ -1,5 +1,6 @@ import csv import io +import json import os import pathlib import tempfile @@ -9,6 +10,7 @@ from typing import Any, Dict, List, NamedTuple, Optional import requests +import salesforce_bulk from cumulusci.core.enums import StrEnum from cumulusci.core.exceptions import BulkDataException @@ -58,6 +60,7 @@ class DataOperationResult(NamedTuple): id: str success: bool error: str + created: Optional[bool] = None class DataOperationJobResult(NamedTuple): @@ -312,6 +315,11 @@ def start(self): """Perform any required setup, such as job initialization, for the operation.""" pass + @abstractmethod + def get_prev_record_values(self, records): + """Get the previous records values in case of UPSERT and UPDATE to prepare for rollback""" + pass + @abstractmethod def load_records(self, records): """Perform the requested DML operation on the supplied row iterator.""" @@ -358,6 +366,56 @@ def end(self): self.bulk.close_job(self.job_id) self.job_result = self._wait_for_job(self.job_id) + def get_prev_record_values(self, records): + """Get the previous values of the records based on the update key + to ensure rollback can be performed""" + # Function to be called only for UPSERT and UPDATE + assert self.operation in [DataOperationType.UPSERT, DataOperationType.UPDATE] + + self.logger.info(f"Retrieving Previous Record Values of {self.sobject}") + prev_record_values = [] + relevant_fields = set(self.fields + ["Id"]) + + # Set update key + update_key = ( + self.api_options.get("update_key") + if self.operation == DataOperationType.UPSERT + else "Id" + ) + + for count, batch in enumerate( + self._batch(records, self.api_options["batch_size"]) + ): + self.context.logger.info(f"Querying batch {count + 1}") + + # Extract update key values from the batch + update_key_values = [ + rec[update_key] + for rec in csv.DictReader([line.decode("utf-8") for line in batch]) + ] + + # Construct the SOQL query + query_fields = ", ".join(relevant_fields) + query_values = ", ".join(f"'{value}'" for value in update_key_values) + query = f"SELECT {query_fields} FROM {self.sobject} WHERE {update_key} IN ({query_values})" + + # Execute the query using Bulk API + job_id = self.bulk.create_query_job(self.sobject, contentType="JSON") + batch_id = self.bulk.query(job_id, query) + self.bulk.wait_for_batch(job_id, batch_id) + self.bulk.close_job(job_id) + results = self.bulk.get_all_results_for_query_batch(batch_id) + + # Extract relevant fields from results and append to the respective lists + for result in results: + result = json.load(salesforce_bulk.util.IteratorBytesIO(result)) + prev_record_values.extend( + [[res[key] for key in relevant_fields] for res in result] + ) + + self.logger.info("Done") + return prev_record_values, tuple(relevant_fields) + def load_records(self, records): self.batch_ids = [] @@ -429,10 +487,12 @@ def get_results(self): for row in reader: success = process_bool_arg(row[1]) + created = process_bool_arg(row[2]) yield DataOperationResult( row[0] if success else None, success, row[3] if not success else None, + created, ) except Exception as e: raise BulkDataException( @@ -489,6 +549,43 @@ def _record_to_json(self, rec): result["attributes"] = {"type": self.sobject} return result + def get_prev_record_values(self, records): + """Get the previous values of the records based on the update key + to ensure rollback can be performed""" + # Function to be called only for UPSERT and UPDATE + assert self.operation in [DataOperationType.UPSERT, DataOperationType.UPDATE] + + self.logger.info(f"Retrieving Previous Record Values of {self.sobject}") + prev_record_values = [] + relevant_fields = set(self.fields + ["Id"]) + + # Set update key + update_key = ( + self.api_options.get("update_key") + if self.operation == DataOperationType.UPSERT + else "Id" + ) + + for chunk in iterate_in_chunks(self.api_options.get("batch_size"), records): + update_key_values = tuple( + filter(None, (self._record_to_json(rec)[update_key] for rec in chunk)) + ) + + # Construct the query string + query_fields = ", ".join(relevant_fields) + query = f"SELECT {query_fields} FROM {self.sobject} WHERE {update_key} IN {update_key_values}" + + # Execute the query + results = self.sf.query(query) + + # Extract relevant fields from results and extend the list + prev_record_values.extend( + [[res[key] for key in relevant_fields] for res in results["records"]] + ) + + self.logger.info("Done") + return prev_record_values, tuple(relevant_fields) + def load_records(self, records): """Load, update, upsert or delete records into the org""" @@ -547,7 +644,14 @@ def _convert(res): else: errors = "" - return DataOperationResult(res.get("id"), res["success"], errors) + if self.operation == DataOperationType.INSERT: + created = True + elif self.operation == DataOperationType.UPDATE: + created = False + else: + created = res.get("created") + + return DataOperationResult(res.get("id"), res["success"], errors, created) yield from (_convert(res) for res in self.results) diff --git a/cumulusci/tasks/bulkdata/tests/test_load.py b/cumulusci/tasks/bulkdata/tests/test_load.py index c6e85fdcb2..a4293910dd 100644 --- a/cumulusci/tasks/bulkdata/tests/test_load.py +++ b/cumulusci/tasks/bulkdata/tests/test_load.py @@ -6,6 +6,7 @@ import shutil import string import tempfile +from collections import namedtuple from contextlib import nullcontext from datetime import date, timedelta from pathlib import Path @@ -18,6 +19,12 @@ from cumulusci.core.exceptions import BulkDataException, TaskOptionsError from cumulusci.salesforce_api.org_schema import get_org_schema from cumulusci.tasks.bulkdata import LoadData +from cumulusci.tasks.bulkdata.load import ( + CreateRollback, + Rollback, + RollbackType, + UpdateRollback, +) from cumulusci.tasks.bulkdata.mapping_parser import MappingLookup, MappingStep from cumulusci.tasks.bulkdata.step import ( BulkApiDmlOperation, @@ -125,6 +132,132 @@ def test_run(self, dml_mock): hh_ids = next(c.execute("SELECT * from households_sf_ids")) assert hh_ids == ("1", "001000000000000") + @responses.activate + @mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation") + def test__insert_rollback(self, dml_mock): + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "start_step": "Insert Contacts", + "set_recently_viewed": False, + "enable_rollback": True, + } + }, + ) + table = mock.Mock() + p = mock.PropertyMock(return_value=f"Contact_{RollbackType.INSERT}") + type(table).name = p + task._initialized_rollback_tables_api = { + f"Contact_{RollbackType.INSERT}": "rest" + } + task.metadata = mock.Mock(sorted_tables=[table]) + task.session = mock.Mock( + query=mock.Mock( + return_value=mock.Mock( + all=mock.Mock(return_value=[mock.Mock(sf_id="00001111")]), + ) + ) + ) + + Rollback._initialized_rollback_tables_api = {"Contact_insert_rollback": "rest"} + CreateRollback._perform_rollback(task, table) + + dml_mock.assert_called_once_with( + sobject="Contact", + operation=(DataOperationType.DELETE), + fields=["Id"], + api_options={}, + context=task, + api="rest", + volume=1, + ) + dml_mock.return_value.start.assert_called_once() + dml_mock.return_value.end.assert_called_once() + + @responses.activate + @mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation") + def test__upsert_rollback(self, dml_mock): + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "start_step": "Upsert Contacts", + "set_recently_viewed": False, + "enable_rollback": True, + } + }, + ) + table = mock.Mock() + p = mock.PropertyMock(return_value=f"Contact_{RollbackType.UPSERT}") + type(table).name = p + Column = namedtuple("Column", ["name"]) + type(table).columns = [Column("Id"), Column("LastName")] + task._initialized_rollback_tables_api = { + f"Contact_{RollbackType.UPSERT}": "rest" + } + task.metadata = mock.Mock(sorted_tables=[table]) + task.session = mock.Mock( + query=mock.Mock( + return_value=mock.Mock( + all=mock.Mock( + return_value=[mock.Mock(Id="00001111", LastName="TestName")] + ), + ) + ) + ) + + Rollback._initialized_rollback_tables_api = {"Contact_upsert_rollback": "rest"} + UpdateRollback._perform_rollback(task, table) + + dml_mock.assert_called_once_with( + sobject="Contact", + operation=(DataOperationType.UPSERT), + fields=["Id", "LastName"], + api_options={"update_key": "Id"}, + context=task, + api="rest", + volume=1, + ) + dml_mock.return_value.start.assert_called_once() + dml_mock.return_value.end.assert_called_once() + + def test__perform_rollback(self): + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "start_step": "Insert Contacts", + "set_recently_viewed": False, + "enable_rollback": True, + } + }, + ) + table_insert = mock.Mock() + p = mock.PropertyMock(return_value=f"Contact_{RollbackType.INSERT}") + type(table_insert).name = p + table_upsert = mock.Mock() + p = mock.PropertyMock(return_value=f"Account_{RollbackType.UPSERT}") + type(table_upsert).name = p + task.metadata = mock.Mock() + task.metadata.sorted_tables = [table_insert, table_upsert] + + with mock.patch.object( + CreateRollback, "_perform_rollback" + ) as mock_insert_rollback, mock.patch.object( + UpdateRollback, "_perform_rollback" + ) as mock_upsert_rollback: + Rollback._perform_rollback(task) + + mock_insert_rollback.assert_called_once_with(task, table_insert) + mock_upsert_rollback.assert_called_once_with(task, table_upsert) + def test_run_task__start_step(self): task = _make_task( LoadData, @@ -1099,6 +1232,7 @@ def test_process_job_results__insert_rows_fail(self): ) task.session = mock.Mock() + task.metadata = mock.MagicMock() task._initialize_id_table = mock.Mock() task.bulk = mock.Mock() task.sf = mock.Mock() @@ -1176,6 +1310,7 @@ def test_process_job_results__exception_failure(self): ) task.session = mock.Mock() + task.metadata = mock.MagicMock() task._initialize_id_table = mock.Mock() task.bulk = mock.Mock() task.sf = mock.Mock() @@ -1529,37 +1664,46 @@ def test_generate_results_id_map__success(self): {"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}}, ) + task.session = mock.MagicMock() + task.metadata = mock.MagicMock() step = mock.Mock() step.get_results.return_value = iter( [ - DataOperationResult("001000000000000", True, None), - DataOperationResult("001000000000001", True, None), - DataOperationResult("001000000000002", True, None), + DataOperationResult("001000000000000", True, None, True), + DataOperationResult("001000000000001", True, None, True), + DataOperationResult("001000000000002", True, None, True), ] ) - generator = task._generate_results_id_map( + sf_id_list = task._generate_results_id_map( step, ["001000000000009", "001000000000010", "001000000000011"] ) - assert list(generator) == [ - ("001000000000009", "001000000000000"), - ("001000000000010", "001000000000001"), - ("001000000000011", "001000000000002"), + assert sf_id_list == [ + ["001000000000009", "001000000000000"], + ["001000000000010", "001000000000001"], + ["001000000000011", "001000000000002"], ] - def test_generate_results_id_map__exception_failure(self): + def test_generate_results_id_map__exception_failure_without_rollback(self): task = _make_task( LoadData, - {"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}}, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "enable_rollback": False, + } + }, ) + task.metadata = mock.MagicMock() step = mock.Mock() step.get_results.return_value = iter( [ - DataOperationResult("001000000000000", True, None), - DataOperationResult(None, False, "error"), - DataOperationResult("001000000000002", True, None), + DataOperationResult("001000000000000", True, None, True), + DataOperationResult(None, False, "error", False), + DataOperationResult("001000000000002", True, None, True), ] ) @@ -1573,6 +1717,43 @@ def test_generate_results_id_map__exception_failure(self): assert "Error on record" in str(e.value) assert "001000000000010" in str(e.value) + def test_generate_results_id_map__exception_failure_with_rollback(self): + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "enable_rollback": True, + } + }, + ) + + task.metadata = mock.MagicMock() + task.session = mock.MagicMock() + step = mock.Mock() + step.get_results.return_value = iter( + [ + DataOperationResult("001000000000000", True, None, True), + DataOperationResult(None, False, "error", False), + DataOperationResult("001000000000002", True, None, True), + ] + ) + + with pytest.raises(BulkDataException) as e, mock.patch( + "cumulusci.tasks.bulkdata.load.Rollback._perform_rollback" + ) as mock_rollback, mock.patch( + "cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records" + ) as mock_insert_records: + task._generate_results_id_map( + step, ["001000000000009", "001000000000010", "001000000000011"] + ) + + mock_rollback.assert_called_once() + mock_insert_records.assert_called_once() + assert "Error on record" in str(e.value) + assert "001000000000010" in str(e.value) + def test_generate_results_id_map__respects_silent_error_flag(self): task = _make_task( LoadData, @@ -1584,17 +1765,17 @@ def test_generate_results_id_map__respects_silent_error_flag(self): } }, ) - + task.metadata = mock.MagicMock() step = mock.Mock() step.get_results.return_value = iter( [DataOperationResult(None, False, None)] * 15 ) with mock.patch.object(task.logger, "warning") as warning: - generator = task._generate_results_id_map( + sf_id_list = task._generate_results_id_map( step, ["001000000000009", "001000000000010", "001000000000011"] * 15 ) - _ = list(generator) # generate the errors + _ = sf_id_list # generate the errors assert len(warning.mock_calls) == task.row_warning_limit + 1 == 11 assert "warnings suppressed" in str(warning.mock_calls[-1]) @@ -1608,15 +1789,106 @@ def test_generate_results_id_map__respects_silent_error_flag(self): ] ) - generator = task._generate_results_id_map( + sf_id_list = task._generate_results_id_map( step, ["001000000000009", "001000000000010", "001000000000011"] ) - assert list(generator) == [ - ("001000000000009", "001000000000000"), - ("001000000000011", "001000000000002"), + assert sf_id_list == [ + ["001000000000009", "001000000000000"], + ["001000000000011", "001000000000002"], ] + @mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation") + def test__execute_step__prev_record_values(self, mock_dml): + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "enable_rollback": True, + } + }, + ) + + ret_prev_records = [["TestName1", "Id1"], ["TestName2", "Id2"]] + ret_columns = ("Name", "Id") + conn = mock.Mock() + task.session = mock.Mock() + task.session.connection.return_value = conn + task.metadata = mock.MagicMock() + tables = {f"Account_{RollbackType.UPSERT}": "AccountUpsertTable"} + task.metadata.tables = tables + step = mock.Mock() + step.fields = ["Name"] + step.sobject = "Account" + query = mock.Mock() + task.configure_step = mock.Mock() + task.configure_step.return_value = (step, query) + step.get_prev_record_values.return_value = (ret_prev_records, ret_columns) + task._load_record_types = mock.Mock() + task._process_job_results = mock.Mock() + task._query_db = mock.Mock() + with mock.patch( + "cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records" + ) as mock_insert_records: + task._execute_step( + MappingStep( + **{ + "sf_object": "Account", + "fields": {"Name": "Name"}, + "action": DataOperationType.UPSERT, + "api": "rest", + "update_key": ["Name"], + } + ) + ) + + mock_insert_records.assert_called_once_with( + connection=conn, + table="AccountUpsertTable", + columns=ret_columns, + record_iterable=ret_prev_records, + ) + + @mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation") + def test__execute_step__job_failure_rollback(self, mock_dml): + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite://", + "mapping": "mapping.yml", + "enable_rollback": True, + } + }, + ) + + task.session = mock.Mock() + task.metadata = mock.MagicMock() + step = mock.Mock() + query = mock.Mock() + step.job_result.status = DataOperationStatus.JOB_FAILURE + task.configure_step = mock.Mock() + task.configure_step.return_value = (step, query) + task._load_record_types = mock.Mock() + task._process_job_results = mock.Mock() + task._query_db = mock.Mock() + with mock.patch( + "cumulusci.tasks.bulkdata.load.Rollback._perform_rollback" + ) as mock_rollback: + task._execute_step( + MappingStep( + **{ + "sf_object": "Account", + "action": DataOperationType.INSERT, + "fields": {"Name": "Name"}, + "api": "rest", + } + ) + ) + mock_rollback.assert_called() + @mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation") def test_execute_step__record_type_mapping(self, dml_mock): task = _make_task( @@ -2614,7 +2886,6 @@ def test_error_result_counting__multi_batches( { "sql_path": cumulusci_test_repo_root / "datasets/bad_sample.sql", "mapping": cumulusci_test_repo_root / "datasets/mapping.yml", - "ignore_row_errors": True, }, ) with mock.patch("cumulusci.tasks.bulkdata.step.DEFAULT_BULK_BATCH_SIZE", 3): @@ -2681,7 +2952,6 @@ def _batch(self, records, n, *args, **kwargs): { "sql_path": sql_path, "mapping": mapping_path, - "ignore_row_errors": True, }, ) task() @@ -2699,7 +2969,6 @@ def test_recreate_set_recent_bug( { "sql_path": cumulusci_test_repo_root / "datasets/sample.sql", "mapping": cumulusci_test_repo_root / "datasets/mapping.yml", - "ignore_row_errors": True, }, ) task.logger = mock.Mock() diff --git a/cumulusci/tasks/bulkdata/tests/test_step.py b/cumulusci/tasks/bulkdata/tests/test_step.py index 786c61c819..fc8cea7013 100644 --- a/cumulusci/tasks/bulkdata/tests/test_step.py +++ b/cumulusci/tasks/bulkdata/tests/test_step.py @@ -498,6 +498,42 @@ def test_serialize_csv_record(self): serialized = step._serialize_csv_record(record) assert serialized == b'"col1","multiline\ncol2"\r\n' + def test_get_prev_record_values(self): + context = mock.Mock() + step = BulkApiDmlOperation( + sobject="Contact", + operation=DataOperationType.UPSERT, + api_options={"batch_size": 10, "update_key": "LastName"}, + context=context, + fields=["LastName"], + ) + results = [ + [{"LastName": "Test1", "Id": "Id1"}, {"LastName": "Test2", "Id": "Id2"}] + ] + expected_record_values = [["Test1", "Id1"], ["Test2", "Id2"]] + expected_relevant_fields = ("Id", "LastName") + step.bulk.create_query_job = mock.Mock() + step.bulk.create_query_job.return_value = "JOB_ID" + step.bulk.query = mock.Mock() + step.bulk.query.return_value = "BATCH_ID" + step.bulk.get_all_results_for_query_batch = mock.Mock() + step.bulk.get_all_results_for_query_batch.return_value = results + + records = iter([["Test1"], ["Test2"], ["Test3"]]) + with mock.patch("json.load", side_effect=lambda result: result), mock.patch( + "salesforce_bulk.util.IteratorBytesIO", side_effect=lambda result: result + ): + prev_record_values, relevant_fields = step.get_prev_record_values(records) + + assert sorted(map(sorted, prev_record_values)) == sorted( + map(sorted, expected_record_values) + ) + assert set(relevant_fields) == set(expected_relevant_fields) + step.bulk.create_query_job.assert_called_once_with( + "Contact", contentType="JSON" + ) + step.bulk.get_all_results_for_query_batch.assert_called_once_with("BATCH_ID") + def test_batch(self): context = mock.Mock() @@ -586,9 +622,9 @@ def test_get_results(self, download_mock): results = step.get_results() assert list(results) == [ - DataOperationResult("003000000000001", True, None), - DataOperationResult("003000000000002", True, None), - DataOperationResult(None, False, "error"), + DataOperationResult("003000000000001", True, None, True), + DataOperationResult("003000000000002", True, None, True), + DataOperationResult(None, False, "error", False), ] download_mock.assert_has_calls( [ @@ -649,9 +685,9 @@ def test_end_to_end(self, download_mock): results = step.get_results() assert list(results) == [ - DataOperationResult("003000000000001", True, None), - DataOperationResult("003000000000002", True, None), - DataOperationResult(None, False, "error"), + DataOperationResult("003000000000001", True, None, True), + DataOperationResult("003000000000002", True, None, True), + DataOperationResult(None, False, "error", False), ] @@ -781,11 +817,68 @@ def test_insert_dml_operation(self): DataOperationStatus.SUCCESS, [], 3, 0 ) assert list(dml_op.get_results()) == [ - DataOperationResult("003000000000001", True, ""), - DataOperationResult("003000000000002", True, ""), - DataOperationResult("003000000000003", True, ""), + DataOperationResult("003000000000001", True, "", True), + DataOperationResult("003000000000002", True, "", True), + DataOperationResult("003000000000003", True, "", True), ] + @responses.activate + def test_get_prev_record_values(self): + mock_describe_calls() + task = _make_task( + LoadData, + { + "options": { + "database_url": "sqlite:///test.db", + "mapping": "mapping.yml", + } + }, + ) + task.project_config.project__package__api_version = CURRENT_SF_API_VERSION + task._init_task() + + responses.add( + responses.POST, + url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects", + json=[ + {"id": "003000000000001", "success": True}, + {"id": "003000000000002", "success": True}, + ], + status=200, + ) + responses.add( + responses.POST, + url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects", + json=[{"id": "003000000000003", "success": True}], + status=200, + ) + + step = RestApiDmlOperation( + sobject="Contact", + operation=DataOperationType.UPSERT, + api_options={"batch_size": 10, "update_key": "LastName"}, + context=task, + fields=["LastName"], + ) + + results = { + "records": [ + {"LastName": "Test1", "Id": "Id1"}, + {"LastName": "Test2", "Id": "Id2"}, + ] + } + expected_record_values = [["Test1", "Id1"], ["Test2", "Id2"]] + expected_relevant_fields = ("Id", "LastName") + step.sf.query = mock.Mock() + step.sf.query.return_value = results + records = iter([["Test1"], ["Test2"], ["Test3"]]) + prev_record_values, relevant_fields = step.get_prev_record_values(records) + + assert sorted(map(sorted, prev_record_values)) == sorted( + map(sorted, expected_record_values) + ) + assert set(relevant_fields) == set(expected_relevant_fields) + @responses.activate def test_insert_dml_operation__boolean_conversion(self): mock_describe_calls() @@ -1013,10 +1106,10 @@ def test_insert_dml_operation__row_failure(self): DataOperationStatus.ROW_FAILURE, [], 3, 1 ) assert list(dml_op.get_results()) == [ - DataOperationResult("003000000000001", True, ""), - DataOperationResult("003000000000002", True, ""), + DataOperationResult("003000000000001", True, "", True), + DataOperationResult("003000000000002", True, "", True), DataOperationResult( - "003000000000003", False, "VALIDATION_ERR: Bad data (FirstName)" + "003000000000003", False, "VALIDATION_ERR: Bad data (FirstName)", True ), ] diff --git a/cumulusci/tasks/bulkdata/tests/test_upsert.py b/cumulusci/tasks/bulkdata/tests/test_upsert.py index f9bf0a9374..aa23c50fcb 100644 --- a/cumulusci/tasks/bulkdata/tests/test_upsert.py +++ b/cumulusci/tasks/bulkdata/tests/test_upsert.py @@ -70,6 +70,7 @@ def _test_two_upserts_and_check_results( / f"datasets/upsert/upsert_mapping_{api}.yml", # "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) @@ -128,6 +129,7 @@ def _test_two_upserts_and_check_results( / f"datasets/upsert/upsert_mapping_{api}.yml", # "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) task() @@ -196,6 +198,7 @@ def test_upsert_rest__faked( / "datasets/upsert/upsert_mapping_rest.yml", "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) task._update_credentials = mock.Mock() @@ -385,6 +388,7 @@ def test_upsert__fake_bulk(self, create_task, cumulusci_test_repo_root, org_conf / "datasets/upsert/upsert_mapping_bulk.yml", "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) task._update_credentials = mock.Mock() @@ -437,6 +441,7 @@ def _test_two_upserts_and_check_results__complex( "mapping": cumulusci_test_repo_root / f"datasets/upsert/upsert_mapping_{api}_complex.yml", "set_recently_viewed": False, + "enable_rollback": False, }, ) task() @@ -457,6 +462,7 @@ def _test_two_upserts_and_check_results__complex( "mapping": cumulusci_test_repo_root / f"datasets/upsert/upsert_mapping_{api}_complex.yml", "set_recently_viewed": False, + "enable_rollback": False, }, ) task() @@ -528,6 +534,7 @@ def test_upsert_complex_external_id_field_rest__duplicate_error( "mapping": cumulusci_test_repo_root / "datasets/upsert/upsert_mapping_rest_complex.yml", "set_recently_viewed": False, + "enable_rollback": False, }, ) task() @@ -656,6 +663,7 @@ def test_simple_upsert__smart( / "datasets/upsert/upsert_smart__native_field.yml", "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) task._update_credentials = mock.Mock() @@ -722,6 +730,7 @@ def test_simple_upsert_smart__native_field( / "datasets/upsert/upsert_smart__native_field.yml", "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) task._update_credentials = mock.Mock() @@ -893,6 +902,7 @@ def test_simple_upsert_smart__non_native_field( / "datasets/upsert/upsert_smart__non_native_field.yml", "ignore_row_errors": True, "set_recently_viewed": False, + "enable_rollback": False, }, ) task._update_credentials = mock.Mock() diff --git a/cumulusci/tasks/bulkdata/tests/utils.py b/cumulusci/tasks/bulkdata/tests/utils.py index 164e26a5e9..173f4c6122 100644 --- a/cumulusci/tasks/bulkdata/tests/utils.py +++ b/cumulusci/tasks/bulkdata/tests/utils.py @@ -92,6 +92,9 @@ def end(self): DataOperationStatus.SUCCESS, [], records_processed, 0 ) + def get_prev_record_values(self, records): + pass + def load_records(self, records): self.records.extend(records) From be0877f941a64ed74176f7fb917ca19286cca430 Mon Sep 17 00:00:00 2001 From: aditya-balachander <139134092+aditya-balachander@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:31:26 +0530 Subject: [PATCH 96/98] Profile Retrieval in Metecho (#3711) [W-14007410](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001ZCxhyYAD/view) Included functionality to retrieve complete profile in Metecho (this is linked to [W-8932343](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07B00000097PhOIAU/view)) --------- Co-authored-by: Naman Jain Co-authored-by: Jaipal Reddy Kasturi --- .../salesforce_api/retrieve_profile_api.py | 8 +- .../tests/test_retrieve_profile_api.py | 6 +- .../tasks/salesforce/retrieve_profile.py | 51 ++++++--- cumulusci/tasks/salesforce/sourcetracking.py | 101 ++++++++++++++---- .../salesforce/tests/test_retrieve_profile.py | 40 ++++++- .../salesforce/tests/test_sourcetracking.py | 67 ++++++++++-- src/package.xml | 0 7 files changed, 221 insertions(+), 52 deletions(-) create mode 100644 src/package.xml diff --git a/cumulusci/salesforce_api/retrieve_profile_api.py b/cumulusci/salesforce_api/retrieve_profile_api.py index 4b100ae542..72aa2c963f 100644 --- a/cumulusci/salesforce_api/retrieve_profile_api.py +++ b/cumulusci/salesforce_api/retrieve_profile_api.py @@ -87,7 +87,9 @@ def __init__( class RetrieveProfileApi(BaseSalesforceApiTask): def _init_task(self): super(RetrieveProfileApi, self)._init_task() - self.api_version = self.org_config.latest_api_version + self.api_version = self.project_config.config["project"]["package"][ + "api_version" + ] def _retrieve_existing_profiles(self, profiles: List[str]): query = self._build_query(["Name"], "Profile", {"Name": profiles}) @@ -97,6 +99,10 @@ def _retrieve_existing_profiles(self, profiles: List[str]): for data in result["records"]: existing_profiles.append(data["Name"]) + # Since System Administrator is named Admin in Metadata API + if "Admin" in profiles: + existing_profiles.extend(["Admin", "System Administrator"]) + return existing_profiles def _run_query(self, query): diff --git a/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py b/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py index 177e1b1b09..99cc67eef3 100644 --- a/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +++ b/cumulusci/salesforce_api/tests/test_retrieve_profile_api.py @@ -18,7 +18,7 @@ def retrieve_profile_api_instance(): project_config = MagicMock() task_config = MagicMock() org_config = MagicMock() - org_config.latest_api_version = "58.0" + project_config.config = {"project": {"package": {"api_version": "58.0"}}} sf_mock.query.return_value = {"records": []} api = RetrieveProfileApi( project_config=project_config, org_config=org_config, task_config=task_config @@ -36,7 +36,7 @@ def test_init_task(retrieve_profile_api_instance): def test_retrieve_existing_profiles(retrieve_profile_api_instance): - profiles = ["Profile1", "Profile2"] + profiles = ["Profile1", "Profile2", "Admin"] result = {"records": [{"Name": "Profile1"}]} with patch.object( RetrieveProfileApi, "_build_query", return_value="some_query" @@ -47,6 +47,8 @@ def test_retrieve_existing_profiles(retrieve_profile_api_instance): assert "Profile1" in existing_profiles assert "Profile2" not in existing_profiles + assert "Admin" in existing_profiles + assert "System Administrator" in existing_profiles def test_run_query_sf(retrieve_profile_api_instance): diff --git a/cumulusci/tasks/salesforce/retrieve_profile.py b/cumulusci/tasks/salesforce/retrieve_profile.py index b9bb3ece8c..db871edb8c 100644 --- a/cumulusci/tasks/salesforce/retrieve_profile.py +++ b/cumulusci/tasks/salesforce/retrieve_profile.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path from cumulusci.core.utils import process_bool_arg, process_list_arg from cumulusci.salesforce_api.metadata import ApiRetrieveUnpackaged @@ -28,21 +28,29 @@ class RetrieveProfile(BaseSalesforceMetadataApiTask): def _init_options(self, kwargs): super(RetrieveProfile, self)._init_options(kwargs) - self.api_version = self.org_config.latest_api_version + self.api_version = self.project_config.config["project"]["package"][ + "api_version" + ] self.profiles = process_list_arg(self.options["profiles"]) if not self.profiles: raise ValueError("At least one profile must be specified.") - self.extract_dir = self.options.get("path", "force-app/default/main") + self.extract_dir = self.options.get("path", "force-app") + extract_path = Path(self.extract_dir) - if not os.path.exists(self.extract_dir): + if not extract_path.exists(): raise FileNotFoundError( f"The extract directory '{self.extract_dir}' does not exist." ) - - if not os.path.isdir(self.extract_dir): + if not extract_path.is_dir(): raise NotADirectoryError(f"'{self.extract_dir}' is not a directory.") + # If extract_dir is force-app and main/default is not present + if self.extract_dir == "force-app": + if not (extract_path / "main" / "default").exists(): + (extract_path / "main" / "default").mkdir(parents=True, exist_ok=True) + self.extract_dir = "force-app/main/default" + self.strictMode = process_bool_arg(self.options.get("strict_mode", True)) def _check_existing_profiles(self, retrieve_profile_api_task): @@ -90,10 +98,25 @@ def add_flow_accesses(self, profile_content, flows): return profile_content def save_profile_file(self, extract_dir, filename, content): - profile_path = os.path.join(extract_dir, filename) - os.makedirs(os.path.dirname(profile_path), exist_ok=True) - with open(profile_path, "w", encoding="utf-8") as updated_profile_file: - updated_profile_file.write(content) + profile_path = Path(extract_dir) / filename + profile_meta_xml_path = Path(extract_dir) / f"{filename}-meta.xml" + + # Check if either the profile file or metadata file exists + if profile_path.exists(): + self.update_file_content(profile_path, content) + elif profile_meta_xml_path.exists(): + self.update_file_content(profile_meta_xml_path, content) + else: + # Neither file exists, create the profile file + profile_meta_xml_path.parent.mkdir(parents=True, exist_ok=True) + with profile_meta_xml_path.open( + mode="w", encoding="utf-8" + ) as updated_profile_file: + updated_profile_file.write(content) + + def update_file_content(self, file_path, content): + with open(file_path, "w", encoding="utf-8") as updated_file: + updated_file.write(content) def _run_task(self): self.retrieve_profile_api_task = RetrieveProfileApi( @@ -126,9 +149,7 @@ def _run_task(self): ) and file_info.filename.endswith(".profile"): with zip_result.open(file_info) as profile_file: profile_content = profile_file.read().decode("utf-8") - profile_name = os.path.splitext( - os.path.basename(file_info.filename) - )[0] + profile_name = profile_name = Path(file_info.filename).stem if profile_name in profile_flows: profile_content = self.add_flow_accesses( @@ -140,7 +161,9 @@ def _run_task(self): ) # zip_result.extractall('./unpackaged') - + self.existing_profiles.remove( + "Admin" + ) if "Admin" in self.existing_profiles else None self.logger.info( f"Profiles {', '.join(self.existing_profiles)} unzipped into folder '{self.extract_dir}'" ) diff --git a/cumulusci/tasks/salesforce/sourcetracking.py b/cumulusci/tasks/salesforce/sourcetracking.py index 68c341b85b..a7c2d02d6f 100644 --- a/cumulusci/tasks/salesforce/sourcetracking.py +++ b/cumulusci/tasks/salesforce/sourcetracking.py @@ -6,11 +6,13 @@ import time from collections import defaultdict -from cumulusci.core.config import ScratchOrgConfig +from cumulusci.core.config import BaseProjectConfig, ScratchOrgConfig, TaskConfig +from cumulusci.core.exceptions import ProjectConfigNotFound from cumulusci.core.sfdx import sfdx from cumulusci.core.utils import process_bool_arg, process_list_arg from cumulusci.tasks.metadata.package import PackageXmlGenerator from cumulusci.tasks.salesforce import BaseRetrieveMetadata, BaseSalesforceApiTask +from cumulusci.tasks.salesforce.retrieve_profile import RetrieveProfile from cumulusci.utils import ( inject_namespace, process_text_in_directory, @@ -167,6 +169,12 @@ def _reset_sfdx_snapshot(self): + " Defaults to project__package__api_version" ) } +retrieve_changes_task_options["retrieve_complete_profile"] = { + "description": ( + "If set to True, will use RetrieveProfile to retrieve" + + " the complete profile. Default is set to False" + ) +} retrieve_changes_task_options["namespace_tokenize"] = BaseRetrieveMetadata.task_options[ "namespace_tokenize" ] @@ -194,6 +202,19 @@ def _write_manifest(changes, path, api_version): f.write(package_xml) +def separate_profiles(components): + """Separate the profiles from components""" + updated_components = [] + profiles = [] + for comp in components: + if comp["MemberType"] == "Profile": + profiles.append(comp["MemberName"]) + else: + updated_components.append(comp) + + return updated_components, profiles + + def retrieve_components( components, org_config, @@ -202,6 +223,8 @@ def retrieve_components( extra_package_xml_opts: dict, namespace_tokenize: str, api_version: str, + project_config: BaseProjectConfig = None, + retrieve_complete_profile: bool = False, ): """Retrieve specified components from an org into a target folder. @@ -215,6 +238,15 @@ def retrieve_components( """ target = os.path.realpath(target) + profiles = [] + + # If retrieve_complete_profile and project_config is None, raise error + # This is because project_config is only required if retrieve_complete_profile is True + if retrieve_complete_profile and project_config is None: + raise ProjectConfigNotFound( + "Kindly provide project_config as part of retrieve_components" + ) + with contextlib.ExitStack() as stack: if md_format: # Create target if it doesn't exist @@ -247,27 +279,47 @@ def retrieve_components( check_return=True, ) - # Construct package.xml with components to retrieve, in its own tempdir - package_xml_path = stack.enter_context(temporary_dir(chdir=False)) - _write_manifest(components, package_xml_path, api_version) - - # Retrieve specified components in DX format - sfdx( - "force:source:retrieve", - access_token=org_config.access_token, - log_note="Retrieving components", - args=[ - "-a", - str(api_version), - "-x", - os.path.join(package_xml_path, "package.xml"), - "-w", - "5", - ], - capture_output=False, - check_return=True, - env={"SFDX_INSTANCE_URL": org_config.instance_url}, - ) + # If retrieve_complete_profile is True, separate the profiles from + # components to retrieve complete profile + if retrieve_complete_profile: + components, profiles = separate_profiles(components) + + if components: + # Construct package.xml with components to retrieve, in its own tempdir + package_xml_path = stack.enter_context(temporary_dir(chdir=False)) + _write_manifest(components, package_xml_path, api_version) + + # Retrieve specified components in DX format + sfdx( + "force:source:retrieve", + access_token=org_config.access_token, + log_note="Retrieving components", + args=[ + "-a", + str(api_version), + "-x", + os.path.join(package_xml_path, "package.xml"), + "-w", + "5", + ], + capture_output=False, + check_return=True, + env={"SFDX_INSTANCE_URL": org_config.instance_url}, + ) + + # Extract Profiles + if profiles: + task_config = TaskConfig( + config={ + "options": {"profiles": ",".join(profiles), "path": "force-app"} + } + ) + cls_retrieve_profile = RetrieveProfile( + org_config=org_config, + project_config=project_config, + task_config=task_config, + ) + cls_retrieve_profile() if md_format: # Convert back to metadata format @@ -304,6 +356,9 @@ class RetrieveChanges(ListChanges, BaseSalesforceApiTask): def _init_options(self, kwargs): super(RetrieveChanges, self)._init_options(kwargs) self.options["snapshot"] = process_bool_arg(kwargs.get("snapshot", True)) + self.options["retrieve_complete_profile"] = process_bool_arg( + self.options.get("retrieve_complete_profile", False) + ) # Check which directories are configured as dx packages package_directories = [] @@ -369,6 +424,8 @@ def _run_task(self): namespace_tokenize=self.options.get("namespace_tokenize"), api_version=self.options["api_version"], extra_package_xml_opts=package_xml_opts, + project_config=self.project_config, + retrieve_complete_profile=self.options["retrieve_complete_profile"], ) if self.options["snapshot"]: diff --git a/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py b/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py index 7f34e39581..2983b4ba32 100644 --- a/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +++ b/cumulusci/tasks/salesforce/tests/test_retrieve_profile.py @@ -130,11 +130,12 @@ def create_temp_zip_file(): return zipfile.ZipFile(temp_zipfile, "r") -def test_save_profile_file(retrieve_profile_task, tmpdir): +def test_save_profile_file_new(retrieve_profile_task, tmpdir): extract_dir = str(tmpdir) filename = "TestProfile.profile" + meta_filename = "TestProfile.profile-meta.xml" content = "Profile content" - expected_file_path = os.path.join(extract_dir, filename) + expected_file_path = os.path.join(extract_dir, meta_filename) retrieve_profile_task.save_profile_file(extract_dir, filename, content) assert os.path.exists(expected_file_path) @@ -143,6 +144,39 @@ def test_save_profile_file(retrieve_profile_task, tmpdir): assert saved_content == content +def test_save_profile_file_existing_meta_xml(retrieve_profile_task, tmpdir): + extract_dir = str(tmpdir) + filename = "TestProfile.profile" + meta_filename = "TestProfile.profile-meta.xml" + content = "Profile content" + existing_file_path = os.path.join(extract_dir, meta_filename) + + with open(existing_file_path, "w", encoding="utf-8") as existing_file: + existing_file.write("Existing content") + + retrieve_profile_task.save_profile_file(extract_dir, filename, content) + + with open(existing_file_path, "r", encoding="utf-8") as profile_file: + saved_content = profile_file.read() + assert saved_content == content + + +def test_save_profile_file_existing(retrieve_profile_task, tmpdir): + extract_dir = str(tmpdir) + filename = "TestProfile.profile" + content = "Profile content" + existing_file_path = os.path.join(extract_dir, filename) + + with open(existing_file_path, "w", encoding="utf-8") as existing_file: + existing_file.write("Existing content") + + retrieve_profile_task.save_profile_file(extract_dir, filename, content) + + with open(existing_file_path, "r", encoding="utf-8") as profile_file: + saved_content = profile_file.read() + assert saved_content == content + + def test_add_flow_accesses(retrieve_profile_task): profile_content = "\n" " Hello\n" "" flows = ["Flow1", "Flow2"] @@ -186,7 +220,7 @@ def test_run_task(retrieve_profile_task, tmpdir, caplog): retrieve_profile_task._run_task() assert os.path.exists(tmpdir) - profile1_path = os.path.join(tmpdir, "profiles/Profile1.profile") + profile1_path = os.path.join(tmpdir, "profiles/Profile1.profile-meta.xml") assert os.path.exists(profile1_path) log_messages = [record.message for record in caplog.records] diff --git a/cumulusci/tasks/salesforce/tests/test_sourcetracking.py b/cumulusci/tasks/salesforce/tests/test_sourcetracking.py index e9f16e7336..97583af20c 100644 --- a/cumulusci/tasks/salesforce/tests/test_sourcetracking.py +++ b/cumulusci/tasks/salesforce/tests/test_sourcetracking.py @@ -3,13 +3,18 @@ import pathlib from unittest import mock +import pytest + from cumulusci.core.config import OrgConfig +from cumulusci.core.exceptions import ProjectConfigNotFound +from cumulusci.tasks.salesforce.retrieve_profile import RetrieveProfile from cumulusci.tasks.salesforce.sourcetracking import ( KNOWN_BAD_MD_TYPES, ListChanges, RetrieveChanges, SnapshotChanges, _write_manifest, + retrieve_components, ) from cumulusci.tests.util import create_project_config from cumulusci.utils import temporary_dir @@ -151,7 +156,12 @@ def test_run_task(self, sfdx, create_task_fixture): with temporary_dir(): task = create_task_fixture( - RetrieveChanges, {"include": "Test", "namespace_tokenize": "ns"} + RetrieveChanges, + { + "include": "Test", + "namespace_tokenize": "ns", + "retrieve_complete_profile": True, + }, ) task._init_task() task.tooling = mock.Mock() @@ -162,18 +172,30 @@ def test_run_task(self, sfdx, create_task_fixture): "MemberType": "CustomObject", "MemberName": "Test__c", "RevisionCounter": 1, - } + }, + { + "MemberType": "Profile", + "MemberName": "TestProfile", + "RevisionCounter": 1, + }, ], } + with mock.patch.object( + RetrieveProfile, "_run_task" + ) as mock_retrieve_profile, mock.patch.object( + pathlib.Path, "exists", return_value=True + ), mock.patch.object( + pathlib.Path, "is_dir", return_value=True + ): + task._run_task() - task._run_task() - - assert sfdx_calls == [ - "force:mdapi:convert", - "force:source:retrieve", - "force:source:convert", - ] - assert os.path.exists(os.path.join("src", "package.xml")) + assert sfdx_calls == [ + "force:mdapi:convert", + "force:source:retrieve", + "force:source:convert", + ] + assert os.path.exists(os.path.join("src", "package.xml")) + mock_retrieve_profile.assert_called() def test_run_task__no_changes(self, sfdx, create_task_fixture): with temporary_dir() as path: @@ -251,3 +273,28 @@ def test_write_manifest__bad_md_types(): assert "Report" in package_xml for name in bad_md_types: assert f"{name}" not in package_xml + + +def test_retrieve_components_project_config_not_found(): + components = mock.Mock() + org_config = mock.Mock() + target = "force-app/" + md_format = False + extra_package_xml_opts = {"sample": "dict"} + namespace_tokenize = "sample" + api_version = "58.0" + expected_error_message = ( + "Kindly provide project_config as part of retrieve_components" + ) + with pytest.raises(ProjectConfigNotFound) as e: + retrieve_components( + components=components, + org_config=org_config, + target=target, + md_format=md_format, + extra_package_xml_opts=extra_package_xml_opts, + namespace_tokenize=namespace_tokenize, + api_version=api_version, + retrieve_complete_profile=True, + ) + assert expected_error_message == e.value.message diff --git a/src/package.xml b/src/package.xml new file mode 100644 index 0000000000..e69de29bb2 From 3f7cc38c24ab27daaf4120f5d69db9e7cbf238bb Mon Sep 17 00:00:00 2001 From: lakshmi2506 <141401869+lakshmi2506@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:25:39 +0530 Subject: [PATCH 97/98] Identifies and retrieves the non source trackable Metadata type components (#3727) [W-14699376](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001gnCZHYA2/view) [W-14698998](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001gmf8ZYAQ/view) [W-14698982](https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00001gmmuuYAA/view) Identifies the list of non source trackable metadata types from the metadata coverage report URL. Later checks whether the metadata types are supported by the org or not. List the components for the individual component and then retrieves the selected components. --- cumulusci/cumulusci.yml | 12 + .../tasks/salesforce/DescribeMetadataTypes.py | 2 +- .../tasks/salesforce/nonsourcetracking.py | 248 ++++++++++++++++ .../tests/test_nonsourcetracking.py | 271 ++++++++++++++++++ 4 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 cumulusci/tasks/salesforce/nonsourcetracking.py create mode 100644 cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml index 9b13e81741..6dd783257a 100644 --- a/cumulusci/cumulusci.yml +++ b/cumulusci/cumulusci.yml @@ -415,6 +415,14 @@ tasks: description: Prints the metadata types in a project class_path: cumulusci.tasks.util.ListMetadataTypes group: Salesforce Metadata + list_nonsource_trackable_components: + description: List the components of non source trackable Metadata types. + class_path: cumulusci.tasks.salesforce.nonsourcetracking.ListComponents + group: Salesforce Metadata + list_nonsource_trackable_metadatatypes: + description: Returns non source trackable metadata types supported by org + class_path: cumulusci.tasks.salesforce.nonsourcetracking.ListNonSourceTrackable + group: Salesforce Metadata meta_xml_apiversion: description: Set the API version in ``*meta.xml`` files class_path: cumulusci.tasks.metaxml.UpdateApi @@ -504,6 +512,10 @@ tasks: description: Retrieve changed components from a scratch org class_path: cumulusci.tasks.salesforce.sourcetracking.RetrieveChanges group: Salesforce Metadata + retrieve_nonsource_trackable: + description: Retrieves the non source trackable components filtered + class_path: cumulusci.tasks.salesforce.nonsourcetracking.RetrieveComponents + group: Salesforce Metadata retrieve_qa_config: description: Retrieves the current changes in the scratch org into unpackaged/config/qa class_path: cumulusci.tasks.salesforce.sourcetracking.RetrieveChanges diff --git a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py index 0b2d4f4e1e..8c0e237e80 100644 --- a/cumulusci/tasks/salesforce/DescribeMetadataTypes.py +++ b/cumulusci/tasks/salesforce/DescribeMetadataTypes.py @@ -23,4 +23,4 @@ def _get_api(self): def _run_task(self): api_object = self._get_api() self.return_values = api_object() - self.logger.info("Metadata Types supported by org:\n" + str(self.return_values)) + return self.return_values diff --git a/cumulusci/tasks/salesforce/nonsourcetracking.py b/cumulusci/tasks/salesforce/nonsourcetracking.py new file mode 100644 index 0000000000..d8c5881e15 --- /dev/null +++ b/cumulusci/tasks/salesforce/nonsourcetracking.py @@ -0,0 +1,248 @@ +import json +import os + +import requests +import sarge + +from cumulusci.core.config import TaskConfig +from cumulusci.core.exceptions import CumulusCIException, SfdxOrgException +from cumulusci.core.sfdx import sfdx +from cumulusci.core.utils import process_list_arg +from cumulusci.tasks.salesforce import ( + BaseRetrieveMetadata, + BaseSalesforceApiTask, + DescribeMetadataTypes, +) +from cumulusci.tasks.salesforce.sourcetracking import ListChanges, retrieve_components + +nl = "\n" + + +class ListNonSourceTrackable(BaseSalesforceApiTask): + + task_options = { + "api_version": { + "description": "Override the API version used to list metadatatypes", + }, + } + + def _init_task(self): + super()._init_task() + if "api_version" not in self.options: + self.options[ + "api_version" + ] = self.project_config.project__package__api_version + + def get_types_details(self, api_version): + # The Metadata coverage report: https://developer.salesforce.com/docs/metadata-coverage/{version} is created from + # the below URL. (So the api versions are allowed based on those report ranges) + url = f"https://dx-extended-coverage.my.salesforce-sites.com/services/apexrest/report?version={api_version}" + response = requests.get(url) + if response.status_code == 200: + json_response = response.json() + return json_response + else: + raise CumulusCIException( + f"Failed to retrieve response with status code {response.status_code}" + ) + + def _run_task(self): + metadatatypes_details = self.get_types_details(self.options["api_version"])[ + "types" + ] + all_nonsource_types = [] + for md_type, details in metadatatypes_details.items(): + if not details["channels"]: + raise CumulusCIException( + f"Api version {self.options['api_version']} not supported" + ) + if ( + details["channels"]["sourceTracking"] is False + and details["channels"]["metadataApi"] is True + ): + all_nonsource_types.append(md_type) + + types_supported = DescribeMetadataTypes( + org_config=self.org_config, + project_config=self.project_config, + task_config=self.task_config, + )._run_task() + + self.return_values = [] + for md_type in all_nonsource_types: + if md_type in types_supported: + self.return_values.append(md_type) + + if self.return_values: + self.return_values.sort() + + self.logger.info( + f"Non source trackable Metadata types supported by org: \n{self.return_values}" + ) + return self.return_values + + +class ListComponents(BaseSalesforceApiTask): + task_options = { + "api_version": { + "description": "Override the API version used to list metadatatypes", + }, + "metadata_types": {"description": "A comma-separated list of metadata types."}, + } + + def _init_task(self): + super()._init_task() + + def _init_options(self, kwargs): + super(ListComponents, self)._init_options(kwargs) + if "api_version" not in self.options: + self.options[ + "api_version" + ] = self.project_config.project__package__api_version + self.options["metadata_types"] = process_list_arg( + self.options.get("metadata_types", []) + ) + + def _get_components(self): + task_config = TaskConfig( + {"options": {"api_version": self.options["api_version"]}} + ) + if not self.options["metadata_types"]: + metadata_types = ListNonSourceTrackable( + org_config=self.org_config, + project_config=self.project_config, + task_config=task_config, + )._run_task() + self.options["metadata_types"] = metadata_types + list_components = [] + for md_type in self.options["metadata_types"]: + p: sarge.Command = sfdx( + "force:mdapi:listmetadata", + access_token=self.org_config.access_token, + log_note="Listing components", + args=[ + "-a", + str(self.options["api_version"]), + "-m", + str(md_type), + "--json", + ], + env={"SFDX_INSTANCE_URL": self.org_config.instance_url}, + ) + stdout, stderr = p.stdout_text.read(), p.stderr_text.read() + + if p.returncode: + message = f"\nstderr:\n{nl.join(stderr)}" + message += f"\nstdout:\n{nl.join(stdout)}" + raise SfdxOrgException(message) + else: + result = json.loads(stdout)["result"] + if result: + for cmp in result: + change_dict = { + "MemberType": md_type, + "MemberName": cmp["fullName"], + "lastModifiedByName": cmp["lastModifiedByName"], + "lastModifiedDate": cmp["lastModifiedDate"], + } + if change_dict not in list_components: + list_components.append(change_dict) + + return list_components + + def _run_task(self): + self.return_values = self._get_components() + self.logger.info( + f"Found {len(self.return_values)} non source trackable components in the org for the given types." + ) + for change in self.return_values: + self.logger.info("{MemberType}: {MemberName}".format(**change)) + return self.return_values + + +retrieve_components_task_options = ListComponents.task_options.copy() +retrieve_components_task_options["path"] = { + "description": "The path to write the retrieved metadata", + "required": False, +} +retrieve_components_task_options["include"] = { + "description": "Components will be included if one of these names" + "is part of either the metadata type or name. " + "Example: ``-o include CustomField,Admin`` matches both " + "``CustomField: Favorite_Color__c`` and ``Profile: Admin``" +} +retrieve_components_task_options["exclude"] = { + "description": "Exclude components matching this name." +} +retrieve_components_task_options[ + "namespace_tokenize" +] = BaseRetrieveMetadata.task_options["namespace_tokenize"] + + +class RetrieveComponents(ListComponents, BaseSalesforceApiTask): + task_options = retrieve_components_task_options + + def _init_options(self, kwargs): + super(RetrieveComponents, self)._init_options(kwargs) + self.options["include"] = process_list_arg(self.options.get("include", [])) + self.options["exclude"] = process_list_arg(self.options.get("exclude", [])) + self._include = self.options["include"] + self._exclude = self.options["exclude"] + self._exclude.extend(self.project_config.project__source__ignore or []) + + package_directories = [] + default_package_directory = None + if os.path.exists("sfdx-project.json"): + with open("sfdx-project.json", "r", encoding="utf-8") as f: + sfdx_project = json.load(f) + for package_directory in sfdx_project.get("packageDirectories", []): + package_directories.append(package_directory["path"]) + if package_directory.get("default"): + default_package_directory = package_directory["path"] + + path = self.options.get("path") + if path is None: + # set default path to src for mdapi format, + # or the default package directory from sfdx-project.json for dx format + if ( + default_package_directory + and self.project_config.project__source_format == "sfdx" + ): + path = default_package_directory + md_format = False + else: + path = "src" + md_format = True + else: + md_format = path not in package_directories + self.md_format = md_format + self.options["path"] = path + + def _run_task(self): + components = self._get_components() + filtered, ignored = ListChanges._filter_changes(self, components) + if not filtered: + self.logger.info("No components to retrieve") + return + for cmp in filtered: + self.logger.info("{MemberType}: {MemberName}".format(**cmp)) + + target = os.path.realpath(self.options["path"]) + package_xml_opts = {} + if self.options["path"] == "src": + package_xml_opts.update( + { + "package_name": self.project_config.project__package__name, + "install_class": self.project_config.project__package__install_class, + "uninstall_class": self.project_config.project__package__uninstall_class, + } + ) + retrieve_components( + filtered, + self.org_config, + target, + md_format=self.md_format, + namespace_tokenize=self.options.get("namespace_tokenize"), + api_version=self.options["api_version"], + extra_package_xml_opts=package_xml_opts, + ) diff --git a/cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py b/cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py new file mode 100644 index 0000000000..a9dde1ca5b --- /dev/null +++ b/cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py @@ -0,0 +1,271 @@ +import io +import json +import os +from unittest import mock + +import pytest +import responses + +from cumulusci.core.exceptions import CumulusCIException, SfdxOrgException +from cumulusci.tasks.salesforce import DescribeMetadataTypes +from cumulusci.tasks.salesforce.nonsourcetracking import ( + ListComponents, + ListNonSourceTrackable, + RetrieveComponents, +) +from cumulusci.tests.util import create_project_config +from cumulusci.utils import temporary_dir + + +class TestListNonSourceTrackable: + @pytest.mark.parametrize( + "json_data, expected_status,error_msg", + [ + (None, 404, "Failed to retrieve response with status code 404"), + ( + {"types": {"Scontrol": {"channels": {}}}}, + 200, + "Api version 44.0 not supported", + ), + ], + ) + @responses.activate + def test_run_get_types_details( + self, create_task_fixture, json_data, expected_status, error_msg + ): + options = {"api_version": 44.0} + responses.add( + "GET", + f"https://dx-extended-coverage.my.salesforce-sites.com/services/apexrest/report?version={options['api_version']}", + json=json_data, + status=expected_status, + ) + task = create_task_fixture(ListNonSourceTrackable, options) + task._init_task() + with pytest.raises(CumulusCIException, match=error_msg): + task._run_task() + + @responses.activate + def test_run_task_list_metadatatypes(self, create_task_fixture): + options = {"api_version": 44.0} + return_value = { + "types": { + "Scontrol": { + "channels": {"sourceTracking": False, "metadataApi": True} + }, + "SharingRules": { + "channels": {"sourceTracking": False, "metadataApi": True} + }, + "ApexClass": { + "channels": {"sourceTracking": True, "metadataApi": True} + }, + "CustomObject": { + "channels": {"sourceTracking": True, "metadataApi": True} + }, + "MessagingChannel": { + "channels": {"sourceTracking": True, "metadataApi": True} + }, + } + } + responses.add( + "GET", + f"https://dx-extended-coverage.my.salesforce-sites.com/services/apexrest/report?version={options['api_version']}", + json=return_value, + status=200, + ) + + task = create_task_fixture(ListNonSourceTrackable, options) + task._init_task() + with mock.patch.object( + DescribeMetadataTypes, + "_run_task", + return_value=["Scontrol", "ApexClass", "CustomObject", "SharingRules"], + ): + non_source_trackable = task._run_task() + assert non_source_trackable == ["Scontrol", "SharingRules"] + + +@mock.patch("sarge.Command") +class TestListComponents: + @pytest.mark.parametrize( + "return_code, result", + [ + ( + 0, + b"""{ + "status": 0, + "result": [], + "warnings": [ + "No metadata found for type: SharingRules" + ] +}""", + ), + (1, b""), + ], + ) + def test_check_sfdx_output(self, cmd, create_task_fixture, return_code, result): + options = {"api_version": 44.0} + task = create_task_fixture(ListComponents, options) + cmd.return_value = mock.Mock( + stderr=io.BytesIO(b""), stdout=io.BytesIO(result), returncode=return_code + ) + task._init_task() + with mock.patch.object( + ListNonSourceTrackable, "_run_task", return_value=["SharingRules"] + ): + if return_code: + with pytest.raises(SfdxOrgException): + task._run_task() + else: + assert task._run_task() == [] + + @pytest.mark.parametrize( + "options", + [ + {"api_version": 44.0, "metadata_types": "FlowDefinition"}, + { + "api_version": 44.0, + }, + ], + ) + def test_check_sfdx_result(self, cmd, create_task_fixture, options): + task = create_task_fixture(ListComponents, options) + result = b"""{ + "status": 0, + "result": [ + { + "createdById": "0051y00000OdeZBAAZ", + "createdByName": "User User", + "createdDate": "2024-01-02T06:50:03.000Z", + "fileName": "flowDefinitions/alpha.flowDefinition", + "fullName": "alpha", + "id": "3001y000000ERX6AAO", + "lastModifiedById": "0051y00000OdeZBAAZ", + "lastModifiedByName": "User User", + "lastModifiedDate": "2024-01-02T06:50:07.000Z", + "manageableState": "unmanaged", + "namespacePrefix": "myalpha", + "type": "FlowDefinition" + },{ + "createdById": "0051y00000OdeZBAAZ", + "createdByName": "User User", + "createdDate": "2024-01-02T06:50:03.000Z", + "fileName": "flowDefinitions/beta.flowDefinition", + "fullName": "beta", + "id": "3001y000000ERX6AAO", + "lastModifiedById": "0051y00000OdeZBAAZ", + "lastModifiedByName": "User User", + "lastModifiedDate": "2024-01-02T06:50:07.000Z", + "manageableState": "unmanaged", + "namespacePrefix": "myalpha", + "type": "FlowDefinition" + } + ], + "warnings": [] +}""" + cmd.return_value = mock.Mock( + stderr=io.BytesIO(b""), stdout=io.BytesIO(result), returncode=0 + ) + messages = [] + task._init_task() + task.logger = mock.Mock() + task.logger.info = messages.append + if "metadata_types" in options: + components = task._run_task() + assert cmd.call_count == 1 + assert ( + "sfdx force:mdapi:listmetadata -a 44.0 -m FlowDefinition --json" + in cmd.call_args[0][0] + ) + assert components == [ + { + "MemberType": "FlowDefinition", + "MemberName": "alpha", + "lastModifiedByName": "User User", + "lastModifiedDate": "2024-01-02T06:50:07.000Z", + }, + { + "MemberType": "FlowDefinition", + "MemberName": "beta", + "lastModifiedByName": "User User", + "lastModifiedDate": "2024-01-02T06:50:07.000Z", + }, + ] + assert ( + "Found 2 non source trackable components in the org for the given types." + in messages + ) + else: + with mock.patch.object( + ListNonSourceTrackable, + "_run_task", + return_value=["Index"], + ): + task._run_task() + assert cmd.call_count == 1 + assert ( + "sfdx force:mdapi:listmetadata -a 44.0 -m Index --json" + in cmd.call_args[0][0] + ) + + +@mock.patch("cumulusci.tasks.salesforce.sourcetracking.sfdx") +class TestRetrieveComponents: + def test_init_options__sfdx_format(self, sfdx, create_task_fixture): + with temporary_dir(): + project_config = create_project_config() + project_config.project__source_format = "sfdx" + with open("sfdx-project.json", "w") as f: + json.dump( + {"packageDirectories": [{"path": "force-app", "default": True}]}, f + ) + task = create_task_fixture(RetrieveComponents, {}, project_config) + assert not task.md_format + assert task.options["path"] == "force-app" + + def test_run_task(self, sfdx, create_task_fixture): + sfdx_calls = [] + sfdx.side_effect = lambda cmd, *args, **kw: sfdx_calls.append(cmd) + + with temporary_dir(): + task = create_task_fixture( + RetrieveComponents, {"include": "alpha", "namespace_tokenize": "ns"} + ) + task._init_task() + with mock.patch.object( + ListComponents, + "_get_components", + return_value=[ + { + "MemberType": "FlowDefinition", + "MemberName": "alpha", + "lastModifiedByName": "User User", + "lastModifiedDate": "2024-01-02T06:50:07.000Z", + }, + { + "MemberType": "FlowDefinition", + "MemberName": "beta", + "lastModifiedByName": "User User", + "lastModifiedDate": "2024-01-02T06:50:07.000Z", + }, + ], + ): + task._run_task() + + assert sfdx_calls == [ + "force:mdapi:convert", + "force:source:retrieve", + "force:source:convert", + ] + assert os.path.exists(os.path.join("src", "package.xml")) + + def test_run_task__no_changes(self, sfdx, create_task_fixture): + with temporary_dir() as path: + task = create_task_fixture(RetrieveComponents, {"path": path}) + task._init_task() + messages = [] + with mock.patch.object(ListComponents, "_get_components", return_value=[]): + task.logger = mock.Mock() + task.logger.info = messages.append + task._run_task() + assert "No components to retrieve" in messages From 68b769ed7689571bb6f69edd75276c7af7b8b944 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Fri, 26 Jan 2024 10:54:50 -0600 Subject: [PATCH 98/98] fix: re-enable telemetry, make CCI usage identifiable (#3728) 1. CLI team would like to not be missing all the CCI usage of the CLI when monitoring performance and making deprecation decisions 2. setting `SFDX_TOOL` makes it obvious which executions come from CCI. Useful for debugging/adoption Co-authored-by: James Estevez --- cumulusci/core/sfdx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulusci/core/sfdx.py b/cumulusci/core/sfdx.py index 87e6ae1e4f..d1c8fd01d7 100644 --- a/cumulusci/core/sfdx.py +++ b/cumulusci/core/sfdx.py @@ -52,7 +52,7 @@ def sfdx( stdout=sarge.Capture(buffer_size=-1) if capture_output else None, stderr=sarge.Capture(buffer_size=-1) if capture_output else None, shell=True, - env={**env, "SFDX_DISABLE_TELEMETRY": "true"}, + env={**env, "SFDX_TOOL": "CCI"}, ) p.run() if capture_output: