castor-extractor 0.17.4__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.

Files changed (79) hide show
  1. CHANGELOG.md +16 -0
  2. castor_extractor/commands/extract_metabase_api.py +2 -1
  3. castor_extractor/commands/extract_metabase_db.py +3 -1
  4. castor_extractor/commands/extract_mode.py +2 -3
  5. castor_extractor/utils/__init__.py +2 -1
  6. castor_extractor/utils/collection.py +8 -0
  7. castor_extractor/utils/safe_request.py +57 -0
  8. castor_extractor/utils/safe_request_test.py +77 -0
  9. castor_extractor/utils/salesforce/__init__.py +1 -2
  10. castor_extractor/utils/salesforce/constants.py +0 -11
  11. castor_extractor/utils/salesforce/credentials.py +22 -45
  12. castor_extractor/visualization/domo/__init__.py +1 -1
  13. castor_extractor/visualization/domo/client/__init__.py +1 -1
  14. castor_extractor/visualization/domo/client/client.py +37 -52
  15. castor_extractor/visualization/domo/client/credentials.py +14 -27
  16. castor_extractor/visualization/domo/extract.py +6 -12
  17. castor_extractor/visualization/looker/__init__.py +6 -1
  18. castor_extractor/visualization/looker/api/__init__.py +2 -1
  19. castor_extractor/visualization/looker/api/client.py +6 -4
  20. castor_extractor/visualization/looker/api/client_test.py +5 -3
  21. castor_extractor/visualization/looker/api/credentials.py +33 -0
  22. castor_extractor/visualization/looker/api/extraction_parameters.py +38 -0
  23. castor_extractor/visualization/looker/api/sdk.py +2 -28
  24. castor_extractor/visualization/looker/constant.py +2 -27
  25. castor_extractor/visualization/looker/constants.py +17 -0
  26. castor_extractor/visualization/looker/extract.py +30 -29
  27. castor_extractor/visualization/metabase/__init__.py +6 -1
  28. castor_extractor/visualization/metabase/client/__init__.py +2 -2
  29. castor_extractor/visualization/metabase/client/api/__init__.py +1 -0
  30. castor_extractor/visualization/metabase/client/api/client.py +8 -14
  31. castor_extractor/visualization/metabase/client/api/credentials.py +13 -40
  32. castor_extractor/visualization/metabase/client/db/__init__.py +1 -0
  33. castor_extractor/visualization/metabase/client/db/client.py +13 -34
  34. castor_extractor/visualization/metabase/client/db/credentials.py +19 -73
  35. castor_extractor/visualization/metabase/errors.py +5 -3
  36. castor_extractor/visualization/metabase/extract.py +1 -1
  37. castor_extractor/visualization/mode/__init__.py +1 -1
  38. castor_extractor/visualization/mode/client/__init__.py +1 -0
  39. castor_extractor/visualization/mode/client/client.py +9 -12
  40. castor_extractor/visualization/mode/client/client_test.py +3 -3
  41. castor_extractor/visualization/mode/client/credentials.py +18 -51
  42. castor_extractor/visualization/mode/extract.py +5 -3
  43. castor_extractor/visualization/powerbi/__init__.py +1 -1
  44. castor_extractor/visualization/powerbi/client/__init__.py +2 -1
  45. castor_extractor/visualization/powerbi/client/credentials.py +17 -9
  46. castor_extractor/visualization/powerbi/client/credentials_test.py +12 -4
  47. castor_extractor/visualization/powerbi/client/rest.py +2 -2
  48. castor_extractor/visualization/powerbi/client/rest_test.py +2 -2
  49. castor_extractor/visualization/powerbi/extract.py +2 -2
  50. castor_extractor/visualization/qlik/__init__.py +5 -1
  51. castor_extractor/visualization/qlik/client/__init__.py +1 -0
  52. castor_extractor/visualization/qlik/client/engine/__init__.py +1 -0
  53. castor_extractor/visualization/qlik/client/engine/client.py +5 -6
  54. castor_extractor/visualization/qlik/client/engine/credentials.py +26 -0
  55. castor_extractor/visualization/qlik/client/master.py +5 -11
  56. castor_extractor/visualization/qlik/client/rest.py +4 -4
  57. castor_extractor/visualization/qlik/client/rest_test.py +6 -2
  58. castor_extractor/visualization/qlik/extract.py +4 -8
  59. castor_extractor/visualization/sigma/__init__.py +1 -1
  60. castor_extractor/visualization/sigma/client/__init__.py +1 -1
  61. castor_extractor/visualization/sigma/client/client.py +5 -4
  62. castor_extractor/visualization/sigma/client/credentials.py +12 -28
  63. castor_extractor/visualization/sigma/extract.py +4 -8
  64. castor_extractor/visualization/tableau_revamp/client/credentials.py +40 -87
  65. castor_extractor/warehouse/redshift/queries/column.sql +0 -5
  66. castor_extractor/warehouse/salesforce/extract.py +2 -2
  67. castor_extractor/warehouse/snowflake/queries/column.sql +0 -1
  68. castor_extractor/warehouse/synapse/queries/column.sql +0 -1
  69. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.2.dist-info}/METADATA +9 -9
  70. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.2.dist-info}/RECORD +73 -73
  71. castor_extractor/visualization/domo/client/client_test.py +0 -60
  72. castor_extractor/visualization/domo/constants.py +0 -6
  73. castor_extractor/visualization/looker/env.py +0 -48
  74. castor_extractor/visualization/looker/parameters.py +0 -78
  75. castor_extractor/visualization/qlik/constants.py +0 -3
  76. castor_extractor/visualization/sigma/constants.py +0 -4
  77. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.2.dist-info}/LICENCE +0 -0
  78. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.2.dist-info}/WHEEL +0 -0
  79. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.2.dist-info}/entry_points.txt +0 -0
@@ -1,80 +1,26 @@
1
- from enum import Enum
2
- from typing import Dict
1
+ from typing import Optional
3
2
 
4
- from .....utils import from_env
3
+ from pydantic import Field, SecretStr
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
5
 
6
+ METABASE_DB_ENV_PREFIX = "CASTOR_METABASE_DB_"
6
7
 
7
- class CredentialsDbKey(Enum):
8
- HOST = "host"
9
- PORT = "port"
10
- DATABASE = "database"
11
- SCHEMA = "schema"
12
- USER = "user"
13
- PASSWORD = "password" # noqa: S105
14
8
 
9
+ class MetabaseDbCredentials(BaseSettings):
10
+ """Metabase's credentials to connect to Metabase DB"""
15
11
 
16
- CREDENTIALS_ENV: Dict[CredentialsDbKey, str] = {
17
- CredentialsDbKey.HOST: "CASTOR_METABASE_DB_HOST",
18
- CredentialsDbKey.PORT: "CASTOR_METABASE_DB_PORT",
19
- CredentialsDbKey.DATABASE: "CASTOR_METABASE_DB_DATABASE",
20
- CredentialsDbKey.SCHEMA: "CASTOR_METABASE_DB_SCHEMA",
21
- CredentialsDbKey.USER: "CASTOR_METABASE_DB_USERNAME",
22
- CredentialsDbKey.PASSWORD: "CASTOR_METABASE_DB_PASSWORD", # noqa: S105
23
- }
12
+ model_config = SettingsConfigDict(
13
+ env_prefix=METABASE_DB_ENV_PREFIX,
14
+ extra="ignore",
15
+ populate_by_name=True,
16
+ )
24
17
 
18
+ host: str
19
+ port: str
20
+ database: str
21
+ schema_: str = Field(validation_alias=f"{METABASE_DB_ENV_PREFIX}SCHEMA")
22
+ user: str = Field(validation_alias=f"{METABASE_DB_ENV_PREFIX}USERNAME")
23
+ password: str = Field(repr=False)
25
24
 
26
- def get_value(key: CredentialsDbKey, kwargs: dict) -> str:
27
- """
28
- Returns the value of the given key:
29
- - from kwargs in priority
30
- - from ENV if not provided (raises an error if not found in ENV)
31
- """
32
- env_key = CREDENTIALS_ENV[key]
33
- return str(kwargs.get(key.value) or from_env(env_key))
34
-
35
-
36
- class CredentialsDb:
37
- """ValueObject for the credentials"""
38
-
39
- def __init__(
40
- self,
41
- host: str,
42
- port: str,
43
- database: str,
44
- schema: str,
45
- user: str,
46
- password: str,
47
- ):
48
- self.host = host
49
- self.port = port
50
- self.database = database
51
- self.schema = schema
52
- self.user = user
53
- self.password = password
54
-
55
- @classmethod
56
- def from_env(cls, env: dict) -> "CredentialsDb":
57
- """returns a new CredentialsDb with values from ENV"""
58
- return CredentialsDb(
59
- host=get_value(CredentialsDbKey.HOST, env),
60
- port=get_value(CredentialsDbKey.PORT, env),
61
- database=get_value(CredentialsDbKey.DATABASE, env),
62
- schema=get_value(CredentialsDbKey.SCHEMA, env),
63
- user=get_value(CredentialsDbKey.USER, env),
64
- password=get_value(CredentialsDbKey.PASSWORD, env),
65
- )
66
-
67
- def to_dict(self, hide: bool = False) -> Dict[str, str]:
68
- safe = (
69
- CredentialsDbKey.HOST,
70
- CredentialsDbKey.PORT,
71
- CredentialsDbKey.DATABASE,
72
- CredentialsDbKey.SCHEMA,
73
- CredentialsDbKey.USER,
74
- )
75
- unsafe = (CredentialsDbKey.PASSWORD,)
76
-
77
- def val(k: CredentialsDbKey, v: str) -> str:
78
- return "*" + v[-4:] if hide and k in unsafe else v
79
-
80
- return {a.value: val(a, getattr(self, a.value)) for a in safe + unsafe}
25
+ encryption_secret_key: Optional[str] = Field(repr=False)
26
+ require_ssl: Optional[str]
@@ -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: dict, error_details: Any):
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: dict, error_details: Any):
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: dict, error_details: Any):
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"
@@ -55,6 +55,6 @@ def extract_all(client: ClientMetabase, **kwargs: str) -> None:
55
55
  write_summary(
56
56
  output_directory,
57
57
  ts,
58
- base_url=client.base_url(),
58
+ base_url=client.base_url,
59
59
  client_name=client.name(),
60
60
  )
@@ -1,3 +1,3 @@
1
1
  from .assets import ModeAnalyticsAsset
2
- from .client import Client
2
+ from .client import Client, ModeCredentials as Credentials
3
3
  from .extract import extract_all, iterate_all_data
@@ -1 +1,2 @@
1
1
  from .client import Client
2
+ from .credentials import ModeCredentials
@@ -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 Credentials, CredentialsKey, get_value
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
- **kwargs,
44
+ credentials: ModeCredentials,
44
45
  ):
45
- self._credentials = 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
- if not kwargs.get("no_checks"):
53
- self._check_connection()
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._credentials.authentication()
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._credentials.authentication()
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
- return Client( # noqa: S106
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 enum import Enum
2
- from typing import Dict
1
+ from pydantic import Field, SecretStr, field_validator
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
3
 
4
- from requests.auth import HTTPBasicAuth
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
- class CredentialsKey(Enum):
10
- HOST = "host"
11
- WORKSPACE = "workspace"
12
- TOKEN = "token" # noqa: S105
13
- SECRET = "secret" # noqa: S105
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
- CREDENTIALS_ENV: Dict[CredentialsKey, str] = {
17
- CredentialsKey.HOST: "CASTOR_MODE_ANALYTICS_HOST",
18
- CredentialsKey.WORKSPACE: "CASTOR_MODE_ANALYTICS_WORKSPACE",
19
- CredentialsKey.TOKEN: "CASTOR_MODE_ANALYTICS_TOKEN",
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(client: Client, **kwargs: str) -> None:
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 = kwargs.get("output_directory") or from_env(OUTPUT_DIR)
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,3 +1,3 @@
1
1
  from .assets import PowerBiAsset
2
- from .client import Client, Credentials
2
+ from .client import Client, PowerbiCredentials, Urls
3
3
  from .extract import extract_all
@@ -1,2 +1,3 @@
1
- from .credentials import Credentials
1
+ from .constants import Urls
2
+ from .credentials import PowerbiCredentials
2
3
  from .rest import Client
@@ -1,20 +1,28 @@
1
- from dataclasses import field
2
1
  from typing import List, Optional
3
2
 
4
- from pydantic.dataclasses import dataclass
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
- @dataclass
10
- class Credentials:
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 = field(metadata={"sensitive": True})
16
- scopes: Optional[List[str]] = None
22
+ secret: str = Field(repr=False)
23
+ scopes: List[str] = [Urls.DEFAULT_SCOPE]
17
24
 
18
- def __post_init__(self):
19
- if self.scopes is None:
20
- self.scopes = [Urls.DEFAULT_SCOPE]
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 Credentials
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 = 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 = 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 = 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 Credentials
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: 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 Credentials
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 = Credentials(
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, Credentials
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 = Credentials(
39
+ creds = PowerbiCredentials(
40
40
  tenant_id=tenant_id,
41
41
  client_id=client_id,
42
42
  secret=secret,
@@ -1,3 +1,7 @@
1
1
  from .assets import QlikAsset
2
- from .client import APP_EXTERNAL_ID_KEY as QLIK_APP_EXTERNAL_ID_KEY, QlikClient
2
+ from .client import (
3
+ APP_EXTERNAL_ID_KEY as QLIK_APP_EXTERNAL_ID_KEY,
4
+ QlikClient,
5
+ QlikCredentials,
6
+ )
3
7
  from .extract import extract_all
@@ -1,2 +1,3 @@
1
1
  from .constants import APP_EXTERNAL_ID_KEY
2
+ from .engine import QlikCredentials
2
3
  from .master import QlikMasterClient as QlikClient
@@ -1 +1,2 @@
1
1
  from .client import EngineApiClient
2
+ from .credentials import QlikCredentials
@@ -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, server_url: str, api_key: str):
53
- self.server_url = server_url
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.server_url,
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
- server_url: str,
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 = 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
- server_url=self._server_url,
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
- server_url: str,
64
- api_key: str,
64
+ credentials: QlikCredentials,
65
65
  except_http_error_statuses: Optional[List[int]] = None,
66
66
  ):
67
- self._server_url = 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
- client = RestApiClient(server_url=dummy_server_url, api_key=dummy_api_key)
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