mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-23 00:18:06 +00:00
Added Salesforce Connector (#423)
* Added salesforce-connector * minor changes * Added Salesforce-connector * Added Salesforce-connector * Salesforce sample data implemented Co-authored-by: parthp2107 <parth.panchal@deuexsoultions.com> Co-authored-by: Ayush Shah <ayush@getcollate.io>
This commit is contained in:
parent
f9319c7265
commit
3965b030a9
36
ingestion/examples/workflows/salesforce.json
Normal file
36
ingestion/examples/workflows/salesforce.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"type": "salesforce",
|
||||||
|
"config": {
|
||||||
|
"username": "username",
|
||||||
|
"password": "password",
|
||||||
|
"security_token": "secuirty_token",
|
||||||
|
"service_name": "local_salesforce",
|
||||||
|
"scheme": "salesforce",
|
||||||
|
"sobject_name": "Salesforce Object Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"processor": {
|
||||||
|
"type": "pii",
|
||||||
|
"config": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sink": {
|
||||||
|
"type": "metadata-rest",
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
"metadata_server": {
|
||||||
|
"type": "metadata-server",
|
||||||
|
"config": {
|
||||||
|
"api_endpoint": "http://localhost:8585/api",
|
||||||
|
"auth_provider_type": "no-auth"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"minute": "*/5",
|
||||||
|
"hour": null,
|
||||||
|
"day": null,
|
||||||
|
"month": null,
|
||||||
|
"day_of_week": null
|
||||||
|
}
|
||||||
|
}
|
@ -108,7 +108,7 @@ class ElasticsearchSink(Sink):
|
|||||||
dashboard_doc = self._create_dashboard_es_doc(record)
|
dashboard_doc = self._create_dashboard_es_doc(record)
|
||||||
self.elasticsearch_client.index(index=self.config.dashboard_index_name, id=str(dashboard_doc.dashboard_id),
|
self.elasticsearch_client.index(index=self.config.dashboard_index_name, id=str(dashboard_doc.dashboard_id),
|
||||||
body=dashboard_doc.json())
|
body=dashboard_doc.json())
|
||||||
self.status.records_written(record.name)
|
self.status.records_written(record.name.__root__)
|
||||||
|
|
||||||
def _create_table_es_doc(self, table: Table):
|
def _create_table_es_doc(self, table: Table):
|
||||||
fqdn = table.fullyQualifiedName
|
fqdn = table.fullyQualifiedName
|
||||||
|
163
ingestion/src/metadata/ingestion/source/salesforce.py
Normal file
163
ingestion/src/metadata/ingestion/source/salesforce.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
# contributor license agreements. See the NOTICE file distributed with
|
||||||
|
# this work for additional information regarding copyright ownership.
|
||||||
|
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
# (the "License"); you may not use this file except in compliance with
|
||||||
|
# the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Iterable, Optional, List
|
||||||
|
|
||||||
|
from metadata.ingestion.api.common import WorkflowContext
|
||||||
|
from metadata.ingestion.api.source import Source, SourceStatus
|
||||||
|
from metadata.ingestion.models.ometa_table_db import OMetaDatabaseAndTable
|
||||||
|
from simple_salesforce import Salesforce
|
||||||
|
|
||||||
|
from .sql_source import SQLConnectionConfig
|
||||||
|
from ..ometa.openmetadata_rest import MetadataServerConfig
|
||||||
|
from ...generated.schema.entity.data.database import Database
|
||||||
|
from ...generated.schema.entity.data.table import Column, ColumnConstraint, Table, TableData
|
||||||
|
from ...generated.schema.type.entityReference import EntityReference
|
||||||
|
from metadata.utils.helpers import get_database_service_or_create
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SalesforceSourceStatus(SourceStatus):
|
||||||
|
success: List[str] = field(default_factory=list)
|
||||||
|
failures: List[str] = field(default_factory=list)
|
||||||
|
warnings: List[str] = field(default_factory=list)
|
||||||
|
filtered: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def scanned(self, table_name: str) -> None:
|
||||||
|
self.success.append(table_name)
|
||||||
|
logger.info('Table Scanned: {}'.format(table_name))
|
||||||
|
|
||||||
|
def filter(
|
||||||
|
self, table_name: str, err: str, dataset_name: str = None, col_type: str = None
|
||||||
|
) -> None:
|
||||||
|
self.filtered.append(table_name)
|
||||||
|
logger.warning("Dropped Table {} due to {}".format(table_name, err))
|
||||||
|
|
||||||
|
|
||||||
|
class SalesforceConfig(SQLConnectionConfig):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
security_token: str
|
||||||
|
host_port: Optional[str]
|
||||||
|
scheme: str
|
||||||
|
service_type = "MySQL"
|
||||||
|
sobject_name: str
|
||||||
|
|
||||||
|
def get_connection_url(self):
|
||||||
|
return super().get_connection_url()
|
||||||
|
|
||||||
|
|
||||||
|
class SalesforceSource(Source):
|
||||||
|
def __init__(self, config: SalesforceConfig, metadata_config: MetadataServerConfig, ctx):
|
||||||
|
super().__init__(ctx)
|
||||||
|
self.config = config
|
||||||
|
self.service = get_database_service_or_create(config, metadata_config)
|
||||||
|
self.status = SalesforceSourceStatus()
|
||||||
|
self.sf = Salesforce(
|
||||||
|
username=self.config.username, password=self.config.password,
|
||||||
|
security_token=self.config.security_token
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, config: dict, metadata_config: dict, ctx: WorkflowContext):
|
||||||
|
config = SalesforceConfig.parse_obj(config)
|
||||||
|
metadata_config = MetadataServerConfig.parse_obj(metadata_config)
|
||||||
|
return cls(config, metadata_config, ctx)
|
||||||
|
|
||||||
|
def column_type(self, column_type: str):
|
||||||
|
if column_type in ["ID", "PHONE", "CURRENCY"]:
|
||||||
|
type = "INT"
|
||||||
|
elif column_type in ["REFERENCE", "PICKLIST", "TEXTAREA", "ADDRESS", "URL"]:
|
||||||
|
type = "VARCHAR"
|
||||||
|
else:
|
||||||
|
type = column_type
|
||||||
|
return type
|
||||||
|
|
||||||
|
def next_record(self) -> Iterable[OMetaDatabaseAndTable]:
|
||||||
|
yield from self.salesforce_client()
|
||||||
|
|
||||||
|
def fetch_sample_data(self,sobject_name):
|
||||||
|
md = self.sf.restful("sobjects/{}/describe/".format(sobject_name), params=None)
|
||||||
|
columns = []
|
||||||
|
rows = []
|
||||||
|
for column in md['fields']:
|
||||||
|
columns.append(column['name'])
|
||||||
|
query = "select {} from {}".format(str(columns)[1:-1].replace('\'',''),sobject_name)
|
||||||
|
logger.info("Ingesting data using {}".format(query))
|
||||||
|
resp = self.sf.query(query)
|
||||||
|
for record in resp['records']:
|
||||||
|
row = []
|
||||||
|
for column in columns:
|
||||||
|
row.append(record[f'{column}'])
|
||||||
|
rows.append(row)
|
||||||
|
return TableData(columns=columns, rows=rows)
|
||||||
|
|
||||||
|
def salesforce_client(self) -> Iterable[OMetaDatabaseAndTable]:
|
||||||
|
try:
|
||||||
|
|
||||||
|
row_order = 1
|
||||||
|
table_columns = []
|
||||||
|
md = self.sf.restful("sobjects/{}/describe/".format(self.config.sobject_name), params=None)
|
||||||
|
|
||||||
|
for column in md['fields']:
|
||||||
|
col_constraint = None
|
||||||
|
if column['nillable']:
|
||||||
|
col_constraint = ColumnConstraint.NULL
|
||||||
|
elif not column['nillable']:
|
||||||
|
col_constraint = ColumnConstraint.NOT_NULL
|
||||||
|
if column['unique']:
|
||||||
|
col_constraint = ColumnConstraint.UNIQUE
|
||||||
|
|
||||||
|
table_columns.append(
|
||||||
|
Column(
|
||||||
|
name=column['name'],
|
||||||
|
description=column['label'],
|
||||||
|
columnDataType=self.column_type(column['type'].upper()),
|
||||||
|
columnConstraint=col_constraint,
|
||||||
|
ordinalPosition=row_order
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_order += 1
|
||||||
|
table_data = self.fetch_sample_data(self.config.sobject_name)
|
||||||
|
logger.info("Successfully Ingested the sample data")
|
||||||
|
table_entity = Table(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
name=self.config.sobject_name,
|
||||||
|
tableType='Regular',
|
||||||
|
description=" ",
|
||||||
|
columns=table_columns,
|
||||||
|
sampleData=table_data
|
||||||
|
)
|
||||||
|
self.status.scanned(f"{self.config.scheme}.{self.config.sobject_name}")
|
||||||
|
database_entity = Database(
|
||||||
|
name=self.config.scheme,
|
||||||
|
service=EntityReference(id=self.service.id, type=self.config.service_type)
|
||||||
|
)
|
||||||
|
table_and_db = OMetaDatabaseAndTable(table=table_entity, database=database_entity)
|
||||||
|
yield table_and_db
|
||||||
|
except ValidationError as err:
|
||||||
|
logger.error(err)
|
||||||
|
self.status.failure('{}'.format(self.config.sobject_name), err)
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_status(self) -> SourceStatus:
|
||||||
|
return self.status
|
Loading…
x
Reference in New Issue
Block a user