diff --git a/dbtmetabase/metabase.py b/dbtmetabase/metabase.py index f880d79..ada8dbc 100644 --- a/dbtmetabase/metabase.py +++ b/dbtmetabase/metabase.py @@ -28,6 +28,8 @@ NullValue, ) +from requests_pkcs12 import Pkcs12Adapter + class MetabaseClient: """Metabase API client.""" @@ -122,13 +124,14 @@ def __init__( password: Optional[str], verify: Optional[Union[str, bool]] = None, cert: Optional[Union[str, Tuple[str, str]]] = None, + pkcs12_data: Optional[Tuple[str, str]] = None, session_id: Optional[str] = None, use_http: bool = False, sync: Optional[bool] = True, sync_timeout: Optional[int] = None, exclude_sources: bool = False, http_extra_headers: Optional[dict] = None, - http_timeout: int = 15, + http_timeout: Optional[int] = 15, ): """Constructor. @@ -141,20 +144,31 @@ def __init__( use_http {bool} -- Use HTTP instead of HTTPS. (default: {False}) verify {Union[str, bool]} -- Path to certificate or disable verification. (default: {None}) cert {Union[str, Tuple[str, str]]} -- Path to a custom certificate to be used by the Metabase client. (default: {None}) + pkcs12_data (Optional[Tuple[str, str]], optional): PKCS#12 Certificate content with its password. If the certificate is not physically present on the running host. (default: {None}) session_id {str} -- Metabase session ID. (default: {None}) sync (bool, optional): Attempt to synchronize Metabase schema with local models. Defaults to True. sync_timeout (Optional[int], optional): Synchronization timeout (in secs). Defaults to None. http_extra_headers {dict} -- HTTP headers to be used by the Metabase client. (default: {None}) exclude_sources {bool} -- Exclude exporting sources. (default: {False}) """ + self.base_url = f"{'http' if use_http else 'https'}://{host}" self.session = requests.Session() self.session.verify = verify self.session.cert = cert + if http_extra_headers is not None: self.session.headers.update(http_extra_headers) + adaptor = HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5)) + + if pkcs12_data is not None: + adaptor = Pkcs12Adapter( + pkcs12_data=pkcs12_data[0], + pkcs12_password=pkcs12_data[1], + ) self.session.mount(self.base_url, adaptor) + self.http_timeout = http_timeout session_header = session_id or self.get_session_id(user, password) self.session.headers["X-Metabase-Session"] = session_header @@ -166,7 +180,6 @@ def __init__( self.table_map: MutableMapping = {} self.models_exposed: List = [] self.native_query: str = "" - self.http_timeout = http_timeout # This regex is looking for from and join clauses, and extracting the table part. # It won't recognize some valid sql table references, such as `from "table with spaces"`. self.exposure_parser = re.compile(r"[FfJj][RrOo][OoIi][MmNn]\s+([\w.\"]+)") diff --git a/dbtmetabase/models/exceptions.py b/dbtmetabase/models/exceptions.py index 730a78d..ccf492e 100644 --- a/dbtmetabase/models/exceptions.py +++ b/dbtmetabase/models/exceptions.py @@ -16,3 +16,7 @@ class MetabaseUnableToSync(Exception): class MetabaseRuntimeError(Exception): """Thrown when Metabase execution failed.""" + + +class MetabaseCertificateImplementationError(Exception): + """Thrown when cert argument and cert_data argument are both defined""" diff --git a/dbtmetabase/models/interface.py b/dbtmetabase/models/interface.py index cdeb37e..cac3dab 100644 --- a/dbtmetabase/models/interface.py +++ b/dbtmetabase/models/interface.py @@ -7,6 +7,7 @@ NoDbtPathSupplied, NoDbtSchemaSupplied, NoMetabaseCredentialsSupplied, + MetabaseCertificateImplementationError, ) from ..parsers.dbt import DbtReader from ..parsers.dbt_folder import DbtFolderReader @@ -29,6 +30,7 @@ def __init__( use_http: bool = False, verify: Optional[Union[str, bool]] = True, cert: Optional[Union[str, Tuple[str, str]]] = None, + pkcs12_data: Optional[Union[str, Tuple[str, str]]] = None, sync: bool = True, sync_timeout: Optional[int] = None, exclude_sources: bool = False, @@ -46,6 +48,7 @@ def __init__( use_http (bool, optional): Use HTTP to connect to Metabase.. Defaults to False. verify (Optional[Union[str, bool]], optional): Path to custom certificate bundle to be used by Metabase client. Defaults to True. cert (Optional[Union[str, Tuple[str, str]]], optional): Path to a custom certificate to be used by the Metabase client, or a tuple containing the path to the certificate and key. Defaults to None. + pkcs12_data (Optional[Tuple[str, str]], optional): PKCS#12 Certificate content with its password. If the certificate is not physically present on the running host. (default: {None}) sync (bool, optional): Attempt to synchronize Metabase schema with local models. Defaults to True. sync_timeout (Optional[int], optional): Synchronization timeout (in secs). Defaults to None. exclude_sources (bool, optional): Exclude exporting sources. Defaults to False. @@ -62,6 +65,7 @@ def __init__( self.use_http = use_http self.verify = verify self.cert = cert + self.pkcs12_data = pkcs12_data self.http_extra_headers = dict(http_extra_headers) if http_extra_headers else {} self.http_timeout = http_timeout # Metabase Sync @@ -98,6 +102,10 @@ def prepare_metabase_client(self, dbt_models: Optional[List[MetabaseModel]] = No raise NoMetabaseCredentialsSupplied( "Credentials or session ID not supplied" ) + if self.cert and self.pkcs12_data: + raise MetabaseCertificateImplementationError( + "cert and pkcs12_data arguments can't be both defined" + ) self._client = MetabaseClient( host=self.host, @@ -106,6 +114,7 @@ def prepare_metabase_client(self, dbt_models: Optional[List[MetabaseModel]] = No use_http=self.use_http, verify=self.verify, cert=self.cert, + pkcs12_data=self.pkcs12_data, http_extra_headers=self.http_extra_headers, session_id=self.session_id, sync=self.sync, @@ -113,7 +122,6 @@ def prepare_metabase_client(self, dbt_models: Optional[List[MetabaseModel]] = No exclude_sources=self.exclude_sources, http_timeout=self.http_timeout, ) - # Sync and attempt schema alignment prior to execution; if timeout is not explicitly set, proceed regardless of success if self.sync: self._client.sync_and_wait(self.database, dbt_models) diff --git a/requirements.txt b/requirements.txt index 6a04699..d636f27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ PyYAML>=5.4.1 requests>=2.26.0 click>=8.0.0 rich>=12.0.0 +requests-pkcs12==1.22