Chore: Tableau Improvements (#21620)

* Chore: Tableau Improvements

* Added apiVersion

* linting

* Addressed Comments
This commit is contained in:
Suman Maharana 2025-06-07 21:38:48 +05:30 committed by GitHub
parent 407eb24baf
commit 161b4a8b2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 59 additions and 30 deletions

View File

@ -13,7 +13,7 @@ Wrapper module of TableauServerConnection client
""" """
import math import math
import traceback import traceback
from typing import Callable, Dict, List, Optional, Tuple, Union from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union
import validators import validators
from cached_property import cached_property from cached_property import cached_property
@ -77,16 +77,18 @@ class TableauClient:
self, self,
tableau_server_auth: Union[PersonalAccessTokenAuth, TableauAuth], tableau_server_auth: Union[PersonalAccessTokenAuth, TableauAuth],
config, config,
verify_ssl, verify_ssl: Union[bool, str],
pagination_limit: int, pagination_limit: int,
): ):
self.tableau_server = Server(str(config.hostPort), use_server_version=True) self.tableau_server = Server(str(config.hostPort), use_server_version=True)
if config.apiVersion:
self.tableau_server.version = config.apiVersion
self.tableau_server.add_http_options({"verify": verify_ssl}) self.tableau_server.add_http_options({"verify": verify_ssl})
self.tableau_server.auth.sign_in(tableau_server_auth) self.tableau_server.auth.sign_in(tableau_server_auth)
self.config = config self.config = config
self.pagination_limit = pagination_limit self.pagination_limit = pagination_limit
self.custom_sql_table_queries: Dict[str, List[str]] = {} self.custom_sql_table_queries: Dict[str, List[str]] = {}
self.usage_metrics: Dict[str, int] = {} self.owner_cache: Dict[str, TableauOwner] = {}
@cached_property @cached_property
def server_info(self) -> Callable: def server_info(self) -> Callable:
@ -101,9 +103,15 @@ class TableauClient:
def get_tableau_owner(self, owner_id: str) -> Optional[TableauOwner]: def get_tableau_owner(self, owner_id: str) -> Optional[TableauOwner]:
try: try:
if owner_id in self.owner_cache:
return self.owner_cache[owner_id]
owner = self.tableau_server.users.get_by_id(owner_id) if owner_id else None owner = self.tableau_server.users.get_by_id(owner_id) if owner_id else None
if owner and owner.email: if owner and owner.email:
return TableauOwner(id=owner.id, name=owner.name, email=owner.email) owner_obj = TableauOwner(
id=owner.id, name=owner.name, email=owner.email
)
self.owner_cache[owner_id] = owner_obj
return owner_obj
except Exception as err: except Exception as err:
logger.debug(f"Failed to fetch owner details for ID {owner_id}: {str(err)}") logger.debug(f"Failed to fetch owner details for ID {owner_id}: {str(err)}")
return None return None
@ -130,21 +138,20 @@ class TableauClient:
) )
view_count += view.total_views view_count += view.total_views
except AttributeError as e: except AttributeError as e:
logger.warning( logger.debug(
f"Failed to process view due to missing attribute: {str(e)}" f"Failed to process view due to missing attribute: {str(e)}"
) )
continue continue
except Exception as e: except Exception as e:
logger.warning(f"Failed to process view: {str(e)}") logger.debug(f"Failed to process view: {str(e)}")
continue continue
return charts, view_count return charts, view_count
def get_workbooks(self) -> List[TableauDashboard]: def get_workbooks(self) -> Iterable[TableauDashboard]:
""" """
Fetch all tableau workbooks Fetch all tableau workbooks
""" """
workbooks: Optional[List[TableauDashboard]] = []
self.cache_custom_sql_tables() self.cache_custom_sql_tables()
for workbook in Pager(self.tableau_server.workbooks): for workbook in Pager(self.tableau_server.workbooks):
try: try:
@ -152,22 +159,20 @@ class TableauClient:
charts, user_views = self.get_workbook_charts_and_user_count( charts, user_views = self.get_workbook_charts_and_user_count(
workbook.views workbook.views
) )
workbooks.append( workbook = TableauDashboard(
TableauDashboard( id=workbook.id,
id=workbook.id, name=workbook.name,
name=workbook.name, project=TableauBaseModel(
project=TableauBaseModel( id=workbook.project_id, name=workbook.project_name
id=workbook.project_id, name=workbook.project_name ),
), owner=self.get_tableau_owner(workbook.owner_id),
description=workbook.description, description=workbook.description,
owner=self.get_tableau_owner(workbook.owner_id), tags=workbook.tags,
tags=workbook.tags, webpageUrl=workbook.webpage_url,
webpageUrl=workbook.webpage_url, charts=charts,
charts=charts, user_views=user_views,
dataModels=self.get_datasources(dashboard_id=workbook.id),
user_views=user_views,
)
) )
yield workbook
except AttributeError as err: except AttributeError as err:
logger.warning( logger.warning(
f"Failed to process workbook due to missing attribute: {str(err)}" f"Failed to process workbook due to missing attribute: {str(err)}"
@ -176,7 +181,6 @@ class TableauClient:
except Exception as err: except Exception as err:
logger.warning(f"Failed to process workbook: {str(err)}") logger.warning(f"Failed to process workbook: {str(err)}")
continue continue
return workbooks
def test_get_workbooks(self): def test_get_workbooks(self):
for workbook in Pager(self.tableau_server.workbooks): for workbook in Pager(self.tableau_server.workbooks):

View File

@ -13,7 +13,7 @@
Source connection handler Source connection handler
""" """
import traceback import traceback
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, Union
import tableauserverclient as TSC import tableauserverclient as TSC
@ -38,7 +38,6 @@ from metadata.ingestion.ometa.ometa_api import OpenMetadata
from metadata.ingestion.source.dashboard.tableau.client import TableauClient from metadata.ingestion.source.dashboard.tableau.client import TableauClient
from metadata.utils.constants import THREE_MIN from metadata.utils.constants import THREE_MIN
from metadata.utils.logger import ingestion_logger from metadata.utils.logger import ingestion_logger
from metadata.utils.ssl_registry import get_verify_ssl_fn
logger = ingestion_logger() logger = ingestion_logger()
@ -48,12 +47,12 @@ def get_connection(connection: TableauConnection) -> TableauClient:
Create connection Create connection
""" """
tableau_server_auth = build_server_config(connection) tableau_server_auth = build_server_config(connection)
get_verify_ssl = get_verify_ssl_fn(connection.verifySSL) verify_ssl = set_verify_ssl(connection)
try: try:
return TableauClient( return TableauClient(
tableau_server_auth=tableau_server_auth, tableau_server_auth=tableau_server_auth,
config=connection, config=connection,
verify_ssl=get_verify_ssl(connection.sslConfig), verify_ssl=verify_ssl,
pagination_limit=connection.paginationLimit, pagination_limit=connection.paginationLimit,
) )
except Exception as exc: except Exception as exc:
@ -63,6 +62,21 @@ def get_connection(connection: TableauConnection) -> TableauClient:
) )
def set_verify_ssl(connection: TableauConnection) -> Union[bool, str]:
"""
Set verify ssl based on connection configuration
ref: https://tableau.github.io/server-client-python/docs/sign-in-out#handling-ssl-certificates-for-tableau-server
"""
if connection.verifySSL.value == "validate":
if connection.sslConfig.root.caCertificate:
return connection.sslConfig.root.caCertificate.get_secret_value()
if connection.sslConfig.root.sslCertificate:
return connection.sslConfig.root.sslCertificate.get_secret_value()
if connection.verifySSL.value == "ignore":
return False
return None
def test_connection( def test_connection(
metadata: OpenMetadata, metadata: OpenMetadata,
client: TableauClient, client: TableauClient,

View File

@ -127,8 +127,8 @@ class TableauSource(DashboardServiceSource):
) )
return cls(config, metadata) return cls(config, metadata)
def get_dashboards_list(self) -> Optional[List[TableauDashboard]]: def get_dashboards_list(self) -> Iterable[TableauDashboard]:
return self.client.get_workbooks() yield from self.client.get_workbooks()
def get_dashboard_name(self, dashboard: TableauDashboard) -> str: def get_dashboard_name(self, dashboard: TableauDashboard) -> str:
return dashboard.name return dashboard.name
@ -137,6 +137,7 @@ class TableauSource(DashboardServiceSource):
""" """
Get Dashboard Details including the dashboard charts and datamodels Get Dashboard Details including the dashboard charts and datamodels
""" """
dashboard.dataModels = self.client.get_datasources(dashboard.id)
return dashboard return dashboard
def get_owner_ref( def get_owner_ref(

View File

@ -52,6 +52,12 @@
"type": "integer", "type": "integer",
"default": 10 "default": 10
}, },
"apiVersion": {
"title": "API Version",
"description": "Tableau API version. If not provided, the version will be used from the tableau server.",
"type": "string",
"default": null
},
"verifySSL": { "verifySSL": {
"$ref": "../../../../security/ssl/verifySSLConfig.json#/definitions/verifySSL", "$ref": "../../../../security/ssl/verifySSLConfig.json#/definitions/verifySSL",
"default": "no-ssl" "default": "no-ssl"

View File

@ -14,6 +14,10 @@
* Tableau Connection Config * Tableau Connection Config
*/ */
export interface TableauConnection { export interface TableauConnection {
/**
* Tableau API version. If not provided, the version will be used from the tableau server.
*/
apiVersion?: string;
/** /**
* Types of methods used to authenticate to the tableau instance * Types of methods used to authenticate to the tableau instance
*/ */