103 lines
4.3 KiB
Python
103 lines
4.3 KiB
Python
#
|
|
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
|
#
|
|
|
|
import logging
|
|
from typing import Any, Iterator, List, Mapping, MutableMapping, Tuple, Union
|
|
|
|
from airbyte_cdk.models import AirbyteCatalog, AirbyteMessage, AirbyteStateMessage, AirbyteStream, ConfiguredAirbyteCatalog, SyncMode
|
|
from airbyte_cdk.sources import AbstractSource
|
|
from airbyte_cdk.sources.streams import Stream
|
|
|
|
from .dataverse import convert_dataverse_type, do_request, get_auth
|
|
from .streams import IncrementalMicrosoftDataverseStream, MicrosoftDataverseStream
|
|
|
|
|
|
class SourceMicrosoftDataverse(AbstractSource):
|
|
def __init__(self):
|
|
self.catalogs = None
|
|
|
|
def discover(self, logger: logging.Logger, config: Mapping[str, Any]) -> AirbyteCatalog:
|
|
response = do_request(config, "EntityDefinitions?$expand=Attributes")
|
|
response_json = response.json()
|
|
streams = []
|
|
for entity in response_json["value"]:
|
|
schema = {"properties": {}}
|
|
for attribute in entity["Attributes"]:
|
|
dataverse_type = attribute["AttributeType"]
|
|
if dataverse_type == "Lookup":
|
|
attribute["LogicalName"] = "_" + attribute["LogicalName"] + "_value"
|
|
attribute_type = convert_dataverse_type(dataverse_type)
|
|
|
|
if not attribute_type:
|
|
continue
|
|
|
|
schema["properties"][attribute["LogicalName"]] = attribute_type
|
|
|
|
if entity["CanChangeTrackingBeEnabled"]["Value"] and entity["ChangeTrackingEnabled"]:
|
|
schema["properties"].update({"_ab_cdc_updated_at": {"type": "string"}, "_ab_cdc_deleted_at": {"type": ["null", "string"]}})
|
|
stream = AirbyteStream(
|
|
name=entity["LogicalName"], json_schema=schema, supported_sync_modes=[SyncMode.full_refresh, SyncMode.incremental]
|
|
)
|
|
stream.source_defined_cursor = True
|
|
if "modifiedon" in schema["properties"]:
|
|
stream.default_cursor_field = ["modifiedon"]
|
|
else:
|
|
stream = AirbyteStream(name=entity["LogicalName"], json_schema=schema, supported_sync_modes=[SyncMode.full_refresh])
|
|
|
|
stream.source_defined_primary_key = [[entity["PrimaryIdAttribute"]]]
|
|
streams.append(stream)
|
|
return AirbyteCatalog(streams=streams)
|
|
|
|
def check_connection(self, logger, config) -> Tuple[bool, any]:
|
|
"""
|
|
:param config: the user-input config object conforming to the connector's spec.yaml
|
|
:param logger: logger object
|
|
:return Tuple[bool, any]: (True, None) if the input config can be used to connect to the API successfully, (False, error) otherwise.
|
|
"""
|
|
try:
|
|
response = do_request(config, "")
|
|
# Raises an exception for error codes (4xx or 5xx)
|
|
response.raise_for_status()
|
|
return True, None
|
|
except Exception as e:
|
|
return False, e
|
|
|
|
def read(
|
|
self,
|
|
logger: logging.Logger,
|
|
config: Mapping[str, Any],
|
|
catalog: ConfiguredAirbyteCatalog,
|
|
state: Union[List[AirbyteStateMessage], MutableMapping[str, Any]] = None,
|
|
) -> Iterator[AirbyteMessage]:
|
|
self.catalogs = catalog
|
|
return super().read(logger, config, catalog, state)
|
|
|
|
def streams(self, config: Mapping[str, Any]) -> List[Stream]:
|
|
"""
|
|
:param config: A Mapping of the user input configuration as defined in the connector spec.
|
|
"""
|
|
auth = get_auth(config)
|
|
|
|
streams = []
|
|
for catalog in self.catalogs.streams:
|
|
response = do_request(config, f"EntityDefinitions(LogicalName='{catalog.stream.name}')")
|
|
response_json = response.json()
|
|
|
|
args = {
|
|
"url": config["url"],
|
|
"stream_name": catalog.stream.name,
|
|
"stream_path": response_json["EntitySetName"],
|
|
"primary_key": catalog.primary_key,
|
|
"schema": catalog.stream.json_schema,
|
|
"odata_maxpagesize": config["odata_maxpagesize"],
|
|
"authenticator": auth,
|
|
}
|
|
|
|
if catalog.sync_mode == SyncMode.incremental:
|
|
streams.append(IncrementalMicrosoftDataverseStream(**args, config_cursor_field=catalog.cursor_field))
|
|
else:
|
|
streams.append(MicrosoftDataverseStream(**args))
|
|
|
|
return streams
|