castor-extractor 0.17.3__py3-none-any.whl → 0.18.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of castor-extractor might be problematic. Click here for more details.
- CHANGELOG.md +20 -0
- castor_extractor/commands/extract_metabase_api.py +2 -1
- castor_extractor/commands/extract_metabase_db.py +3 -1
- castor_extractor/commands/extract_mode.py +2 -3
- castor_extractor/utils/__init__.py +2 -1
- castor_extractor/utils/collection.py +8 -0
- castor_extractor/utils/safe_request.py +57 -0
- castor_extractor/utils/safe_request_test.py +77 -0
- castor_extractor/utils/salesforce/__init__.py +1 -2
- castor_extractor/utils/salesforce/constants.py +0 -11
- castor_extractor/utils/salesforce/credentials.py +22 -45
- castor_extractor/visualization/domo/__init__.py +1 -1
- castor_extractor/visualization/domo/client/__init__.py +1 -1
- castor_extractor/visualization/domo/client/client.py +37 -52
- castor_extractor/visualization/domo/client/credentials.py +14 -27
- castor_extractor/visualization/domo/extract.py +6 -12
- castor_extractor/visualization/looker/__init__.py +6 -1
- castor_extractor/visualization/looker/api/__init__.py +2 -1
- castor_extractor/visualization/looker/api/client.py +6 -4
- castor_extractor/visualization/looker/api/client_test.py +5 -3
- castor_extractor/visualization/looker/api/credentials.py +33 -0
- castor_extractor/visualization/looker/api/extraction_parameters.py +38 -0
- castor_extractor/visualization/looker/api/sdk.py +2 -28
- castor_extractor/visualization/looker/constant.py +4 -21
- castor_extractor/visualization/looker/constants.py +17 -0
- castor_extractor/visualization/looker/extract.py +30 -29
- castor_extractor/visualization/metabase/__init__.py +6 -1
- castor_extractor/visualization/metabase/client/__init__.py +2 -2
- castor_extractor/visualization/metabase/client/api/__init__.py +1 -0
- castor_extractor/visualization/metabase/client/api/client.py +8 -14
- castor_extractor/visualization/metabase/client/api/credentials.py +13 -40
- castor_extractor/visualization/metabase/client/db/__init__.py +1 -0
- castor_extractor/visualization/metabase/client/db/client.py +13 -34
- castor_extractor/visualization/metabase/client/db/credentials.py +19 -73
- castor_extractor/visualization/metabase/errors.py +5 -3
- castor_extractor/visualization/metabase/extract.py +1 -1
- castor_extractor/visualization/mode/__init__.py +1 -1
- castor_extractor/visualization/mode/client/__init__.py +1 -0
- castor_extractor/visualization/mode/client/client.py +9 -12
- castor_extractor/visualization/mode/client/client_test.py +3 -3
- castor_extractor/visualization/mode/client/credentials.py +18 -51
- castor_extractor/visualization/mode/extract.py +5 -3
- castor_extractor/visualization/powerbi/__init__.py +1 -1
- castor_extractor/visualization/powerbi/client/__init__.py +2 -1
- castor_extractor/visualization/powerbi/client/credentials.py +17 -9
- castor_extractor/visualization/powerbi/client/credentials_test.py +12 -4
- castor_extractor/visualization/powerbi/client/rest.py +2 -2
- castor_extractor/visualization/powerbi/client/rest_test.py +2 -2
- castor_extractor/visualization/powerbi/extract.py +2 -2
- castor_extractor/visualization/qlik/__init__.py +5 -1
- castor_extractor/visualization/qlik/client/__init__.py +1 -0
- castor_extractor/visualization/qlik/client/engine/__init__.py +1 -0
- castor_extractor/visualization/qlik/client/engine/client.py +5 -6
- castor_extractor/visualization/qlik/client/engine/credentials.py +26 -0
- castor_extractor/visualization/qlik/client/master.py +5 -11
- castor_extractor/visualization/qlik/client/rest.py +4 -4
- castor_extractor/visualization/qlik/client/rest_test.py +6 -2
- castor_extractor/visualization/qlik/extract.py +4 -8
- castor_extractor/visualization/sigma/__init__.py +1 -1
- castor_extractor/visualization/sigma/client/__init__.py +1 -1
- castor_extractor/visualization/sigma/client/client.py +12 -5
- castor_extractor/visualization/sigma/client/credentials.py +12 -28
- castor_extractor/visualization/sigma/extract.py +4 -8
- castor_extractor/visualization/tableau_revamp/client/credentials.py +40 -87
- castor_extractor/warehouse/redshift/queries/column.sql +0 -5
- castor_extractor/warehouse/salesforce/extract.py +2 -2
- castor_extractor/warehouse/snowflake/queries/column.sql +0 -1
- castor_extractor/warehouse/synapse/queries/column.sql +0 -1
- {castor_extractor-0.17.3.dist-info → castor_extractor-0.18.2.dist-info}/METADATA +9 -9
- {castor_extractor-0.17.3.dist-info → castor_extractor-0.18.2.dist-info}/RECORD +73 -73
- castor_extractor/visualization/domo/client/client_test.py +0 -60
- castor_extractor/visualization/domo/constants.py +0 -6
- castor_extractor/visualization/looker/env.py +0 -48
- castor_extractor/visualization/looker/parameters.py +0 -78
- castor_extractor/visualization/qlik/constants.py +0 -3
- castor_extractor/visualization/sigma/constants.py +0 -4
- {castor_extractor-0.17.3.dist-info → castor_extractor-0.18.2.dist-info}/LICENCE +0 -0
- {castor_extractor-0.17.3.dist-info → castor_extractor-0.18.2.dist-info}/WHEEL +0 -0
- {castor_extractor-0.17.3.dist-info → castor_extractor-0.18.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class MetabaseLoginError(ConnectionError):
|
|
5
|
-
def __init__(self, credentials_info:
|
|
7
|
+
def __init__(self, credentials_info: BaseSettings, error_details: Any):
|
|
6
8
|
msg = "Connection to Metabase failed!\n"
|
|
7
9
|
msg += f"Credentials: {credentials_info}\n"
|
|
8
10
|
msg += f"Details: {error_details}\n"
|
|
@@ -10,7 +12,7 @@ class MetabaseLoginError(ConnectionError):
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class SuperuserCredentialsRequired(AssertionError):
|
|
13
|
-
def __init__(self, credentials_info:
|
|
15
|
+
def __init__(self, credentials_info: BaseSettings, error_details: Any):
|
|
14
16
|
msg = "Superuser credentials are required!\n"
|
|
15
17
|
msg += f"Credentials: {credentials_info}\n"
|
|
16
18
|
msg += f"Details: {error_details}\n"
|
|
@@ -22,7 +24,7 @@ class EncryptionSecretKeyRequired(AssertionError):
|
|
|
22
24
|
Raised when missing the encryption secret key while it was required.
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
|
-
def __init__(self, credentials_info:
|
|
27
|
+
def __init__(self, credentials_info: BaseSettings, error_details: Any):
|
|
26
28
|
msg = "Encryption secret key is required!\n"
|
|
27
29
|
msg += f"Credentials: {credentials_info}\n"
|
|
28
30
|
msg += f"Details: {error_details}\n"
|
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
from typing import Dict, List, Optional, cast
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
|
+
from requests.auth import HTTPBasicAuth
|
|
5
6
|
|
|
6
7
|
from ....utils import retry
|
|
7
8
|
from ..assets import (
|
|
@@ -22,7 +23,7 @@ from .constants import (
|
|
|
22
23
|
RETRY_JITTER_MS,
|
|
23
24
|
RETRY_STRATEGY,
|
|
24
25
|
)
|
|
25
|
-
from .credentials import
|
|
26
|
+
from .credentials import ModeCredentials
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -40,20 +41,16 @@ class Client:
|
|
|
40
41
|
|
|
41
42
|
def __init__(
|
|
42
43
|
self,
|
|
43
|
-
|
|
44
|
+
credentials: ModeCredentials,
|
|
44
45
|
):
|
|
45
|
-
self._credentials =
|
|
46
|
-
host=get_value(CredentialsKey.HOST, kwargs).rstrip("/"),
|
|
47
|
-
workspace=get_value(CredentialsKey.WORKSPACE, kwargs),
|
|
48
|
-
token=get_value(CredentialsKey.TOKEN, kwargs),
|
|
49
|
-
secret=get_value(CredentialsKey.SECRET, kwargs),
|
|
50
|
-
)
|
|
46
|
+
self._credentials = credentials
|
|
51
47
|
self._session = requests.Session()
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
|
|
49
|
+
def authenticate(self) -> HTTPBasicAuth:
|
|
50
|
+
return HTTPBasicAuth(self._credentials.token, self._credentials.secret)
|
|
54
51
|
|
|
55
52
|
def _check_connection(self):
|
|
56
|
-
authentication = self.
|
|
53
|
+
authentication = self.authenticate()
|
|
57
54
|
url = self._url(with_workspace=False) + "/account"
|
|
58
55
|
response = self._session.get(url, auth=authentication)
|
|
59
56
|
self._handle_response(response)
|
|
@@ -121,7 +118,7 @@ class Client:
|
|
|
121
118
|
report: Optional[str] = None,
|
|
122
119
|
resource_name: Optional[str] = None,
|
|
123
120
|
) -> RawData:
|
|
124
|
-
authentication = self.
|
|
121
|
+
authentication = self.authenticate()
|
|
125
122
|
url = self._url(with_workspace, space, report, resource_name)
|
|
126
123
|
logger.info(f"Calling {url}")
|
|
127
124
|
response = self._session.get(url, auth=authentication)
|
|
@@ -2,20 +2,20 @@ import json
|
|
|
2
2
|
|
|
3
3
|
from ....utils import load_file
|
|
4
4
|
from ..assets import EXPORTED_FIELDS, ModeAnalyticsAsset
|
|
5
|
-
from .client import Client
|
|
5
|
+
from .client import Client, ModeCredentials
|
|
6
6
|
|
|
7
7
|
_HOST = "https://mode.com"
|
|
8
8
|
_WORKSPACE = "castor"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _dummy_client() -> Client:
|
|
12
|
-
|
|
12
|
+
credentials = ModeCredentials(
|
|
13
13
|
host=_HOST,
|
|
14
14
|
workspace=_WORKSPACE,
|
|
15
15
|
token="dummy-token",
|
|
16
16
|
secret="******",
|
|
17
|
-
no_checks=True,
|
|
18
17
|
)
|
|
18
|
+
return Client(credentials=credentials)
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test__url():
|
|
@@ -1,57 +1,24 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from pydantic import Field, SecretStr, field_validator
|
|
2
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
MODE_ENV_PREFIX = "CASTOR_MODE_ANALYTICS_"
|
|
5
5
|
|
|
6
|
-
from ....utils import from_env
|
|
7
6
|
|
|
7
|
+
class ModeCredentials(BaseSettings):
|
|
8
|
+
"""Class holding Mode credentials attributes"""
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
model_config = SettingsConfigDict(
|
|
11
|
+
env_prefix=MODE_ENV_PREFIX,
|
|
12
|
+
extra="ignore",
|
|
13
|
+
populate_by_name=True,
|
|
14
|
+
)
|
|
14
15
|
|
|
16
|
+
host: str
|
|
17
|
+
secret: str = Field(repr=False)
|
|
18
|
+
token: str
|
|
19
|
+
workspace: str
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
CredentialsKey.SECRET: "CASTOR_MODE_ANALYTICS_SECRET",
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_value(key: CredentialsKey, kwargs: dict) -> str:
|
|
25
|
-
"""
|
|
26
|
-
Returns the value of the given key:
|
|
27
|
-
- from kwargs in priority
|
|
28
|
-
- from ENV if not provided (raises an error if not found in ENV)
|
|
29
|
-
"""
|
|
30
|
-
env_key = CREDENTIALS_ENV[key]
|
|
31
|
-
return str(kwargs.get(key.value) or from_env(env_key))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class Credentials:
|
|
35
|
-
"""ValueObject for the credentials"""
|
|
36
|
-
|
|
37
|
-
def __init__(self, host: str, workspace: str, token: str, secret: str):
|
|
38
|
-
self.host = host
|
|
39
|
-
self.workspace = workspace
|
|
40
|
-
self.token = token
|
|
41
|
-
self.secret = secret
|
|
42
|
-
|
|
43
|
-
def to_dict(self, hide: bool = False) -> Dict[str, str]:
|
|
44
|
-
safe = (
|
|
45
|
-
CredentialsKey.HOST,
|
|
46
|
-
CredentialsKey.WORKSPACE,
|
|
47
|
-
CredentialsKey.TOKEN,
|
|
48
|
-
)
|
|
49
|
-
unsafe = (CredentialsKey.SECRET,)
|
|
50
|
-
|
|
51
|
-
def val(k: CredentialsKey, v: str) -> str:
|
|
52
|
-
return "*" + v[-4:] if hide and k in unsafe else v
|
|
53
|
-
|
|
54
|
-
return {a.value: val(a, getattr(self, a.value)) for a in safe + unsafe}
|
|
55
|
-
|
|
56
|
-
def authentication(self) -> HTTPBasicAuth:
|
|
57
|
-
return HTTPBasicAuth(self.token, self.secret)
|
|
21
|
+
@field_validator("host", mode="before")
|
|
22
|
+
@classmethod
|
|
23
|
+
def _check_base_url(cls, host: str) -> str:
|
|
24
|
+
return host.rstrip("/")
|
|
@@ -11,7 +11,7 @@ from ...utils import (
|
|
|
11
11
|
write_summary,
|
|
12
12
|
)
|
|
13
13
|
from .assets import ModeAnalyticsAsset as Asset
|
|
14
|
-
from .client import Client
|
|
14
|
+
from .client import Client, ModeCredentials
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -37,9 +37,11 @@ def iterate_all_data(
|
|
|
37
37
|
yield Asset.MEMBER, deep_serialize(members)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def extract_all(
|
|
40
|
+
def extract_all(credentials: ModeCredentials, output_directory: str) -> None:
|
|
41
41
|
"""Extract Data From Mode Analytics and store it locally in files under the output_directory"""
|
|
42
|
-
output_directory =
|
|
42
|
+
output_directory = output_directory or from_env(OUTPUT_DIR)
|
|
43
|
+
|
|
44
|
+
client = Client(credentials)
|
|
43
45
|
ts = current_timestamp()
|
|
44
46
|
|
|
45
47
|
for key, data in iterate_all_data(client):
|
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
from dataclasses import field
|
|
2
1
|
from typing import List, Optional
|
|
3
2
|
|
|
4
|
-
from pydantic
|
|
3
|
+
from pydantic import Field, field_validator
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
5
|
|
|
6
6
|
from .constants import Urls
|
|
7
7
|
|
|
8
|
+
POWERBI_ENV_PREFIX = "CASTOR_POWERBI_"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
class
|
|
10
|
+
|
|
11
|
+
class PowerbiCredentials(BaseSettings):
|
|
11
12
|
"""Class to handle PowerBI rest API permissions"""
|
|
12
13
|
|
|
14
|
+
model_config = SettingsConfigDict(
|
|
15
|
+
env_prefix=POWERBI_ENV_PREFIX,
|
|
16
|
+
extra="ignore",
|
|
17
|
+
populate_by_name=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
13
20
|
client_id: str
|
|
14
21
|
tenant_id: str
|
|
15
|
-
secret: str =
|
|
16
|
-
scopes:
|
|
22
|
+
secret: str = Field(repr=False)
|
|
23
|
+
scopes: List[str] = [Urls.DEFAULT_SCOPE]
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
@field_validator("scopes", mode="before")
|
|
26
|
+
@classmethod
|
|
27
|
+
def _check_scopes(cls, scopes: Optional[List[str]]) -> List[str]:
|
|
28
|
+
return scopes if scopes is not None else [Urls.DEFAULT_SCOPE]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .constants import Urls
|
|
2
|
-
from .credentials import
|
|
2
|
+
from .credentials import PowerbiCredentials
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def test_credentials():
|
|
@@ -8,15 +8,23 @@ def test_credentials():
|
|
|
8
8
|
secret = "🤫"
|
|
9
9
|
|
|
10
10
|
# no scopes provided
|
|
11
|
-
credentials =
|
|
11
|
+
credentials = PowerbiCredentials(
|
|
12
12
|
tenant_id=tenant_id,
|
|
13
13
|
client_id=client_id,
|
|
14
14
|
secret=secret,
|
|
15
15
|
)
|
|
16
16
|
assert credentials.scopes == [Urls.DEFAULT_SCOPE]
|
|
17
17
|
|
|
18
|
+
credentials = PowerbiCredentials(
|
|
19
|
+
tenant_id=tenant_id,
|
|
20
|
+
client_id=client_id,
|
|
21
|
+
secret=secret,
|
|
22
|
+
scopes=None,
|
|
23
|
+
)
|
|
24
|
+
assert credentials.scopes == [Urls.DEFAULT_SCOPE]
|
|
25
|
+
|
|
18
26
|
# empty scopes
|
|
19
|
-
credentials =
|
|
27
|
+
credentials = PowerbiCredentials(
|
|
20
28
|
tenant_id=tenant_id,
|
|
21
29
|
client_id=client_id,
|
|
22
30
|
secret=secret,
|
|
@@ -26,7 +34,7 @@ def test_credentials():
|
|
|
26
34
|
|
|
27
35
|
# with scopes
|
|
28
36
|
scopes = ["foo"]
|
|
29
|
-
credentials =
|
|
37
|
+
credentials = PowerbiCredentials(
|
|
30
38
|
tenant_id=tenant_id,
|
|
31
39
|
client_id=client_id,
|
|
32
40
|
secret=secret,
|
|
@@ -18,7 +18,7 @@ from .constants import (
|
|
|
18
18
|
QueryParams,
|
|
19
19
|
Urls,
|
|
20
20
|
)
|
|
21
|
-
from .credentials import
|
|
21
|
+
from .credentials import PowerbiCredentials
|
|
22
22
|
from .utils import batch_size_is_valid_or_assert, datetime_is_recent_or_assert
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
@@ -62,7 +62,7 @@ class Client:
|
|
|
62
62
|
https://learn.microsoft.com/en-us/rest/api/power-bi/admin
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
|
-
def __init__(self, credentials:
|
|
65
|
+
def __init__(self, credentials: PowerbiCredentials):
|
|
66
66
|
self.creds = credentials
|
|
67
67
|
client_app = f"{Urls.CLIENT_APP_BASE}{self.creds.tenant_id}"
|
|
68
68
|
self.app = msal.ConfidentialClientApplication(
|
|
@@ -5,7 +5,7 @@ import pytest
|
|
|
5
5
|
from requests import HTTPError
|
|
6
6
|
|
|
7
7
|
from .constants import GET, POST, Assertions, Keys, QueryParams, Urls
|
|
8
|
-
from .credentials import
|
|
8
|
+
from .credentials import PowerbiCredentials
|
|
9
9
|
from .rest import Client, msal
|
|
10
10
|
|
|
11
11
|
FAKE_TENANT_ID = "IamFake"
|
|
@@ -14,7 +14,7 @@ FAKE_SECRET = "MeThree"
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _client() -> Client:
|
|
17
|
-
creds =
|
|
17
|
+
creds = PowerbiCredentials(
|
|
18
18
|
tenant_id=FAKE_TENANT_ID,
|
|
19
19
|
client_id=FAKE_CLIENT_ID,
|
|
20
20
|
secret=FAKE_SECRET,
|
|
@@ -10,7 +10,7 @@ from ...utils import (
|
|
|
10
10
|
write_summary,
|
|
11
11
|
)
|
|
12
12
|
from .assets import METADATA_ASSETS, PowerBiAsset
|
|
13
|
-
from .client import Client,
|
|
13
|
+
from .client import Client, PowerbiCredentials
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def iterate_all_data(
|
|
@@ -36,7 +36,7 @@ def extract_all(
|
|
|
36
36
|
Store the output files locally under the given output_directory
|
|
37
37
|
"""
|
|
38
38
|
_output_directory = output_directory or from_env(OUTPUT_DIR)
|
|
39
|
-
creds =
|
|
39
|
+
creds = PowerbiCredentials(
|
|
40
40
|
tenant_id=tenant_id,
|
|
41
41
|
client_id=client_id,
|
|
42
42
|
secret=secret,
|
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
from .....utils import SafeMode, safe_mode
|
|
4
4
|
from .constants import MEASURES_SESSION_PARAMS, JsonRpcMethod
|
|
5
|
+
from .credentials import QlikCredentials
|
|
5
6
|
from .error import AccessDeniedError, AppSizeExceededError
|
|
6
7
|
from .json_rpc import JsonRpcClient
|
|
7
8
|
from .websocket import open_websocket
|
|
@@ -49,10 +50,8 @@ class EngineApiClient:
|
|
|
49
50
|
get measures using JsonRpcClient and websocket connection.
|
|
50
51
|
"""
|
|
51
52
|
|
|
52
|
-
def __init__(self,
|
|
53
|
-
self.
|
|
54
|
-
self.api_key = api_key
|
|
55
|
-
|
|
53
|
+
def __init__(self, credentials: QlikCredentials):
|
|
54
|
+
self.credentials = credentials
|
|
56
55
|
self._safe_mode = SafeMode(
|
|
57
56
|
exceptions=(AccessDeniedError, AppSizeExceededError),
|
|
58
57
|
max_errors=float("inf"),
|
|
@@ -70,8 +69,8 @@ class EngineApiClient:
|
|
|
70
69
|
|
|
71
70
|
with open_websocket(
|
|
72
71
|
app_id=app_id,
|
|
73
|
-
server_url=self.
|
|
74
|
-
api_key=self.api_key,
|
|
72
|
+
server_url=self.credentials.base_url,
|
|
73
|
+
api_key=self.credentials.api_key,
|
|
75
74
|
) as websocket:
|
|
76
75
|
json_rpc_client = JsonRpcClient(websocket=websocket)
|
|
77
76
|
return _call(json_rpc_client, app_id)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import Field, SecretStr, field_validator
|
|
2
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
3
|
+
|
|
4
|
+
from .....utils import validate_baseurl
|
|
5
|
+
|
|
6
|
+
QLIK_ENV_PREFIX = "CASTOR_QLIK_"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QlikCredentials(BaseSettings):
|
|
10
|
+
"""
|
|
11
|
+
Qlik's credentials to connect to the API
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
model_config = SettingsConfigDict(
|
|
15
|
+
env_prefix=QLIK_ENV_PREFIX,
|
|
16
|
+
extra="ignore",
|
|
17
|
+
populate_by_name=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
api_key: str = Field(repr=False)
|
|
21
|
+
base_url: str
|
|
22
|
+
|
|
23
|
+
@field_validator("base_url", mode="before")
|
|
24
|
+
@classmethod
|
|
25
|
+
def _check_base_url(cls, base_url: str) -> str:
|
|
26
|
+
return validate_baseurl(base_url)
|
|
@@ -4,7 +4,7 @@ from tqdm import tqdm # type: ignore
|
|
|
4
4
|
|
|
5
5
|
from ..assets import QlikAsset
|
|
6
6
|
from .constants import APP_EXTERNAL_ID_KEY, SCOPED_ASSETS
|
|
7
|
-
from .engine import EngineApiClient
|
|
7
|
+
from .engine import EngineApiClient, QlikCredentials
|
|
8
8
|
from .rest import RestApiClient
|
|
9
9
|
|
|
10
10
|
ListedData = List[dict]
|
|
@@ -53,25 +53,19 @@ class QlikMasterClient:
|
|
|
53
53
|
|
|
54
54
|
def __init__(
|
|
55
55
|
self,
|
|
56
|
-
|
|
57
|
-
api_key: str,
|
|
56
|
+
credentials: QlikCredentials,
|
|
58
57
|
except_http_error_statuses: Optional[List[int]] = None,
|
|
59
58
|
display_progress: bool = True,
|
|
60
59
|
):
|
|
61
|
-
self._server_url =
|
|
62
|
-
self._api_key = api_key
|
|
60
|
+
self._server_url = credentials.base_url
|
|
63
61
|
self.display_progress = display_progress
|
|
64
62
|
|
|
65
63
|
self.rest_api_client = RestApiClient(
|
|
66
|
-
|
|
67
|
-
api_key=self._api_key,
|
|
64
|
+
credentials=credentials,
|
|
68
65
|
except_http_error_statuses=except_http_error_statuses,
|
|
69
66
|
)
|
|
70
67
|
|
|
71
|
-
self.engine_api_client = EngineApiClient(
|
|
72
|
-
server_url=self._server_url,
|
|
73
|
-
api_key=self._api_key,
|
|
74
|
-
)
|
|
68
|
+
self.engine_api_client = EngineApiClient(credentials=credentials)
|
|
75
69
|
|
|
76
70
|
def _fetch_lineage(self, apps: ListedData) -> ListedData:
|
|
77
71
|
callback = self.rest_api_client.data_lineage
|
|
@@ -16,6 +16,7 @@ from .constants import (
|
|
|
16
16
|
RETRY_COUNTS,
|
|
17
17
|
RETRY_STATUSES,
|
|
18
18
|
)
|
|
19
|
+
from .engine import QlikCredentials
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -60,12 +61,11 @@ class RestApiClient:
|
|
|
60
61
|
|
|
61
62
|
def __init__(
|
|
62
63
|
self,
|
|
63
|
-
|
|
64
|
-
api_key: str,
|
|
64
|
+
credentials: QlikCredentials,
|
|
65
65
|
except_http_error_statuses: Optional[List[int]] = None,
|
|
66
66
|
):
|
|
67
|
-
self._server_url =
|
|
68
|
-
self._api_key = api_key
|
|
67
|
+
self._server_url = credentials.base_url
|
|
68
|
+
self._api_key = credentials.api_key
|
|
69
69
|
self._session = _session()
|
|
70
70
|
self._except_http_error_statuses = except_http_error_statuses or []
|
|
71
71
|
self._authenticate()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
from unittest.mock import call, patch
|
|
3
3
|
|
|
4
|
+
from .engine import QlikCredentials
|
|
4
5
|
from .rest import RestApiClient
|
|
5
6
|
|
|
6
7
|
|
|
@@ -22,8 +23,11 @@ def _check_called_once(
|
|
|
22
23
|
def test_rest_api_client_pager():
|
|
23
24
|
dummy_server_url = "https://clic.kom"
|
|
24
25
|
dummy_api_key = "i-am-the-key-dont-let-others-know-about"
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
credentials = QlikCredentials(
|
|
27
|
+
base_url=dummy_server_url,
|
|
28
|
+
api_key=dummy_api_key,
|
|
29
|
+
)
|
|
30
|
+
client = RestApiClient(credentials=credentials)
|
|
27
31
|
|
|
28
32
|
first_page_url = "https://clic.kom/assets"
|
|
29
33
|
|
|
@@ -7,13 +7,11 @@ from ...utils import (
|
|
|
7
7
|
deep_serialize,
|
|
8
8
|
from_env,
|
|
9
9
|
get_output_filename,
|
|
10
|
-
validate_baseurl,
|
|
11
10
|
write_json,
|
|
12
11
|
write_summary,
|
|
13
12
|
)
|
|
14
13
|
from .assets import QlikAsset
|
|
15
|
-
from .client import QlikClient
|
|
16
|
-
from .constants import API_KEY, BASE_URL
|
|
14
|
+
from .client import QlikClient, QlikCredentials
|
|
17
15
|
|
|
18
16
|
logger = logging.getLogger(__name__)
|
|
19
17
|
|
|
@@ -59,13 +57,11 @@ def extract_all(
|
|
|
59
57
|
Store the output files locally under the given output_directory
|
|
60
58
|
"""
|
|
61
59
|
|
|
60
|
+
credentials = QlikCredentials(base_url=base_url, api_key=api_key)
|
|
62
61
|
_output_directory = output_directory or from_env(OUTPUT_DIR)
|
|
63
|
-
_base_url = validate_baseurl(base_url or from_env(BASE_URL))
|
|
64
|
-
_api_key = api_key or from_env(API_KEY)
|
|
65
62
|
|
|
66
63
|
client = QlikClient(
|
|
67
|
-
|
|
68
|
-
api_key=_api_key,
|
|
64
|
+
credentials=credentials,
|
|
69
65
|
except_http_error_statuses=except_http_error_statuses,
|
|
70
66
|
)
|
|
71
67
|
|
|
@@ -75,4 +71,4 @@ def extract_all(
|
|
|
75
71
|
filename = get_output_filename(key.name.lower(), _output_directory, ts)
|
|
76
72
|
write_json(filename, data)
|
|
77
73
|
|
|
78
|
-
write_summary(_output_directory, ts, base_url=
|
|
74
|
+
write_summary(_output_directory, ts, base_url=credentials.base_url)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .client import SigmaClient
|
|
2
|
-
from .credentials import
|
|
2
|
+
from .credentials import SigmaCredentials
|
|
@@ -5,14 +5,20 @@ from urllib.parse import urljoin
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
7
|
from ..assets import SigmaAsset
|
|
8
|
-
from .credentials import
|
|
8
|
+
from .credentials import SigmaCredentials
|
|
9
9
|
from .endpoints import EndpointFactory
|
|
10
10
|
from .pagination import Pagination
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
DATA_ELEMENTS: Tuple[str,
|
|
15
|
+
DATA_ELEMENTS: Tuple[str, ...] = (
|
|
16
|
+
"input-table",
|
|
17
|
+
"pivot-table",
|
|
18
|
+
"table",
|
|
19
|
+
"visualization",
|
|
20
|
+
"viz",
|
|
21
|
+
)
|
|
16
22
|
_CONTENT_TYPE = "application/x-www-form-urlencoded"
|
|
17
23
|
|
|
18
24
|
|
|
@@ -23,6 +29,7 @@ class SigmaClient:
|
|
|
23
29
|
self.host = credentials.host
|
|
24
30
|
self.client_id = credentials.client_id
|
|
25
31
|
self.api_token = credentials.api_token
|
|
32
|
+
self.grant_type = credentials.grant_type
|
|
26
33
|
self.headers: Optional[Dict[str, str]] = None
|
|
27
34
|
|
|
28
35
|
def _get_token(self) -> Dict[str, str]:
|
|
@@ -31,9 +38,9 @@ class SigmaClient:
|
|
|
31
38
|
token_response = requests.post( # noqa: S113
|
|
32
39
|
token_api_path,
|
|
33
40
|
data={
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
"grant_type": self.grant_type,
|
|
42
|
+
"client_id": self.client_id,
|
|
43
|
+
"client_secret": self.api_token,
|
|
37
44
|
},
|
|
38
45
|
)
|
|
39
46
|
if token_response.status_code != requests.codes.OK:
|