From bffbed8c7b79fa223b7b61596bb01b36123e5158 Mon Sep 17 00:00:00 2001 From: soustruh <martin@struzsky.cz> Date: Fri, 7 Mar 2025 11:24:20 +0100 Subject: [PATCH 1/3] =?UTF-8?q?ignore=20flake8's=20E203=20warning=20(space?= =?UTF-8?q?=20before=20colon)=20+=20.venv=20folder=20=E2=9D=84=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funnily enough, Flake8's E203 is not PEP8 compliant and it's even stated in Flake8's official configuration example. 😏 Sources: https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices https://flake8.pycqa.org/en/latest/user/configuration.html https://stackoverflow.com/questions/75614339/black-and-flake8-hooks-conflicts --- flake8.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake8.cfg b/flake8.cfg index b577af2..56d457d 100644 --- a/flake8.cfg +++ b/flake8.cfg @@ -1,6 +1,8 @@ [flake8] +extend-ignore = E203 exclude = .git, + .venv, __pycache__, tests, example From bec13f21d82096f1056203479e34b4aa72523901 Mon Sep 17 00:00:00 2001 From: soustruh <martin@struzsky.cz> Date: Fri, 7 Mar 2025 11:24:28 +0100 Subject: [PATCH 2/3] sync action for the `run` method --- src/component.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/component.py b/src/component.py index 1fd940c..6db7214 100644 --- a/src/component.py +++ b/src/component.py @@ -8,7 +8,7 @@ from os import mkdir, path from keboola.component.base import ComponentBase, sync_action -from keboola.component.dao import SupportedDataTypes, BaseType, ColumnDefinition +from keboola.component.dao import BaseType, ColumnDefinition, SupportedDataTypes from keboola.component.exceptions import UserException from keboola.component.sync_actions import MessageType, SelectElement, ValidationResult from keboola.utils.header_normalizer import NormalizerStrategy, get_normalizer @@ -86,7 +86,7 @@ class Component(ComponentBase): def __init__(self): super().__init__() - def run(self): + def run(self, return_data=False): self.validate_configuration_parameters(REQUIRED_PARAMETERS) self.validate_image_parameters(REQUIRED_IMAGE_PARS) @@ -130,6 +130,9 @@ def run(self): logging.debug([result for result in results]) logging.info(f"Downloaded {total_records} records in total") + if return_data: + return results + # remove headers and get columns output_columns = self._fix_header_from_csv(results) output_columns = self.normalize_column_names(output_columns) @@ -419,9 +422,9 @@ def _get_login_method(self) -> LoginType: @staticmethod def process_salesforce_domain(url): if url.startswith("http://"): - url = url[len("http://"):] + url = url[len("http://") :] if url.startswith("https://"): - url = url[len("https://"):] + url = url[len("https://") :] if url.endswith(".salesforce.com"): url = url[: -len(".salesforce.com")] @@ -619,6 +622,13 @@ def load_possible_primary_keys(self) -> list[SelectElement]: else: raise UserException(f"Invalid {KEY_QUERY_TYPE}") + # @sync_action("runComponent") + # def sync_run_component(self): + # """ + # Run the component as a sync action + # """ + # return self.run(return_data=True) + def _get_object_name_from_custom_query(self) -> str: params = self.configuration.parameters salesforce_client = self.get_salesforce_client(params) From 158261a8bbfa6214fffe3918045c93b4378b2c43 Mon Sep 17 00:00:00 2001 From: soustruh <martin@struzsky.cz> Date: Tue, 11 Mar 2025 08:01:09 +0100 Subject: [PATCH 3/3] =?UTF-8?q?listSkills=20sync=20action=20defining=20the?= =?UTF-8?q?=20available=20AI=20agent=20skills=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eventually the response of this sync action shall be generated dynamically based on component configuration --- .github/workflows/push.yml | 2 +- src/component.py | 147 +++++++++++++++++++++++++++++++++++-- 2 files changed, 140 insertions(+), 9 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index a986c0d..75449cb 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -67,7 +67,7 @@ jobs: TAG="${GITHUB_REF##*/}" IS_SEMANTIC_TAG=$(echo "$TAG" | grep -q '^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$' && echo true || echo false) echo "is_semantic_tag=$IS_SEMANTIC_TAG" | tee -a $GITHUB_OUTPUT - echo "app_image_tag=$TAG" | tee -a $GITHUB_OUTPUT + echo "app_image_tag=$TAG-${{ github.run_id }}" | tee -a $GITHUB_OUTPUT - name: Deploy-Ready check id: deploy_ready diff --git a/src/component.py b/src/component.py index 6db7214..90597a0 100644 --- a/src/component.py +++ b/src/component.py @@ -1,4 +1,5 @@ import csv +import json import logging import os import shutil @@ -60,6 +61,115 @@ DEFAULT_LOGIN_METHOD = "security_token" +# the listSkills response shall be generated based on user config in a very near future, +# so I just hardcoded it here for now +LIST_SKILLS_STATIC_RESPONSE = [ + { + "name": "Get Salesforce contacts", + "description": "Returns list of Salesforce contacts.", + "parameters": [ + { + "name": "action", + "type": "string", + "required": True, + "enum": ["runSkill"], + }, + { + "name": "parameters", + "type": "object", + "required": True, + "parameters": { + "name": "configData", + "type": "object", + "required": True, + "parameters": [ + { + "name": "object", + "type": "string", + "required": True, + "enum": ["Contact"], + "description": "String identifer of the agent skill to run.", + }, + ], + }, + }, + ], + "response": { + "type": "object", + "properties": { + "status": { + "title": "Response status", + "type": "string", + "enum": ["ok", "error"], + }, + "number_of_records": { + "title": "Number of records in the response", + "type": "integer", + }, + "records": { + "title": "Salesforce records", + "type": "array", + "items": { + "type": "object", + }, + }, + }, + }, + }, + { + "name": "Get Salesforce reports", + "description": "Returns list of Salesforce reports.", + "parameters": [ + { + "name": "action", + "type": "string", + "required": True, + "enum": ["runSkill"], + }, + { + "name": "parameters", + "type": "object", + "required": True, + "parameters": { + "name": "configData", + "type": "object", + "required": True, + "parameters": [ + { + "name": "object", + "type": "string", + "required": True, + "enum": ["Report"], + "description": "String identifer of the agent skill to run.", + }, + ], + }, + }, + ], + "response": { + "type": "object", + "properties": { + "status": { + "title": "Response status", + "type": "string", + "enum": ["ok", "error"], + }, + "number_of_records": { + "title": "Number of records in the response", + "type": "integer", + }, + "records": { + "title": "Salesforce records", + "type": "array", + "items": { + "type": "object", + }, + }, + }, + }, + }, +] + class LoginType(str, Enum): SECURITY_TOKEN_LOGIN = "security_token" @@ -86,13 +196,16 @@ class Component(ComponentBase): def __init__(self): super().__init__() - def run(self, return_data=False): + def run(self, return_json=False): self.validate_configuration_parameters(REQUIRED_PARAMETERS) self.validate_image_parameters(REQUIRED_IMAGE_PARS) start_run_time = str(datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")) params = self.configuration.parameters + + logging.info("%s", json.dumps(params, indent=4)) + loading_options = params.get(KEY_LOADING_OPTIONS, {}) bucket_name = params.get(KEY_BUCKET_NAME, self.get_bucket_name()) @@ -130,7 +243,15 @@ def run(self, return_data=False): logging.debug([result for result in results]) logging.info(f"Downloaded {total_records} records in total") - if return_data: + if return_json: + for result in results: + if "file" not in result: + result["status"] = "fileError" + continue + result["records"] = list(self._csv_result_to_dict(result.pop("file"))) + result["status"] = "ok" + with open("response.json", "w") as f: + json.dump(results, f) return results # remove headers and get columns @@ -180,6 +301,12 @@ def _fix_header_from_csv(results: list[dict]) -> list[str]: os.replace(temp_file_path, result_file_path) return expected_header + def _csv_result_to_dict(self, filename: str): + with open(filename) as f: + reader = csv.DictReader(f) + for row in reader: + yield row + def set_proxy(self) -> None: """Sets proxy if defined""" proxy_config = self.configuration.parameters.get(KEY_PROXY, {}) @@ -622,12 +749,16 @@ def load_possible_primary_keys(self) -> list[SelectElement]: else: raise UserException(f"Invalid {KEY_QUERY_TYPE}") - # @sync_action("runComponent") - # def sync_run_component(self): - # """ - # Run the component as a sync action - # """ - # return self.run(return_data=True) + @sync_action("runSkill") + def sync_run_component(self): + """ + Run the component as a skill for an AI agent + """ + return self.run(return_json=True) + + @sync_action("listSkills") + def list_skills(self): + return LIST_SKILLS_STATIC_RESPONSE def _get_object_name_from_custom_query(self) -> str: params = self.configuration.parameters