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
CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.2 - 2024-07-08
4
+
5
+ * Added StatusCode handling to SafeMode
6
+
7
+ ## 0.18.1 - 2024-07-04
8
+
9
+ * Bump dependencies: numpy, setuptools, tableauserverclient
10
+
11
+ ## 0.18.0 - 2024-07-03
12
+
13
+ * Dashboarding technologies : Reworked credentials using Pydantic
14
+
15
+ ## 0.17.5 - 2024-07-03
16
+
17
+ * Snowflake, Synapse, Redshift: Remove default_value from the extracted column
18
+
3
19
  ## 0.17.4 - 2024-07-03
4
20
 
5
21
  * Sigma: Add `input-table`, `pivot-table` and `viz` in the list of supported **Elements**
@@ -17,11 +17,12 @@ def main():
17
17
 
18
18
  args = parser.parse_args()
19
19
 
20
- client = metabase.ApiClient(
20
+ credentials = metabase.MetabaseApiCredentials(
21
21
  base_url=args.base_url,
22
22
  user=args.username,
23
23
  password=args.password,
24
24
  )
25
+ client = metabase.ApiClient(credentials)
25
26
 
26
27
  metabase.extract_all(
27
28
  client,
@@ -35,7 +35,7 @@ def main():
35
35
 
36
36
  args = parser.parse_args()
37
37
 
38
- client = metabase.DbClient(
38
+ credentials = metabase.MetabaseDbCredentials(
39
39
  host=args.host,
40
40
  port=args.port,
41
41
  database=args.database,
@@ -46,6 +46,8 @@ def main():
46
46
  require_ssl=args.require_ssl,
47
47
  )
48
48
 
49
+ client = metabase.DbClient(credentials)
50
+
49
51
  metabase.extract_all(
50
52
  client,
51
53
  output_directory=args.output,
@@ -25,8 +25,7 @@ def main():
25
25
  parser.add_argument("-o", "--output", help="Directory to write to")
26
26
 
27
27
  args = parser.parse_args()
28
-
29
- client = mode.Client(
28
+ credentials = mode.Credentials(
30
29
  host=args.host,
31
30
  workspace=args.workspace,
32
31
  token=args.token,
@@ -34,6 +33,6 @@ def main():
34
33
  )
35
34
 
36
35
  mode.extract_all(
37
- client,
36
+ credentials,
38
37
  output_directory=args.output,
39
38
  )
@@ -5,7 +5,7 @@ from .client import (
5
5
  SqlalchemyClient,
6
6
  uri_encode,
7
7
  )
8
- from .collection import group_by, mapping_from_rows
8
+ from .collection import empty_iterator, group_by, mapping_from_rows
9
9
  from .constants import OUTPUT_DIR
10
10
  from .deprecate import deprecate_python
11
11
  from .env import from_env
@@ -23,6 +23,7 @@ from .pager import (
23
23
  )
24
24
  from .retry import RetryStrategy, retry
25
25
  from .safe import SafeMode, safe_mode
26
+ from .safe_request import RequestSafeMode, ResponseJson, handle_response
26
27
  from .store import AbstractStorage, LocalStorage
27
28
  from .string import decode_when_bytes, string_to_tuple
28
29
  from .time import (
@@ -44,3 +44,11 @@ def mapping_from_rows(rows: List[Dict], key: Any, value: Any) -> Dict:
44
44
  mapping[mapping_key] = mapping_value
45
45
 
46
46
  return mapping
47
+
48
+
49
+ def empty_iterator():
50
+ """
51
+ Utils to return empty iterator, mainly used for viz transformers
52
+ Remark: missing return type is on purpose, it breaks the typing
53
+ """
54
+ return iter([])
@@ -0,0 +1,57 @@
1
+ import logging
2
+ from typing import List, Tuple, Union
3
+
4
+ from requests import HTTPError, Response
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ ResponseJson = Union[dict, List[dict]]
9
+
10
+
11
+ class RequestSafeMode:
12
+ """
13
+ RequestSafeMode class to parameterize what should be done if response
14
+ raises due to the status code.
15
+
16
+ Attributes:
17
+ self.status_codes: tuple of status codes that will be caught
18
+ self.errors_caught : list of errors caught
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ max_errors: Union[int, float] = 0,
24
+ status_codes: Tuple[int, ...] = (),
25
+ ):
26
+ self.max_errors = max_errors
27
+ self.status_codes: List[int] = list(status_codes)
28
+ self.status_codes_caught: List[int] = []
29
+
30
+ def catch_response(self, exception: HTTPError, status_code: int):
31
+ if int(status_code) not in self.status_codes:
32
+ raise exception
33
+
34
+ self.status_codes_caught.append(int(status_code))
35
+
36
+ @property
37
+ def should_raise(self) -> bool:
38
+ return len(self.status_codes_caught) > self.max_errors
39
+
40
+
41
+ def handle_response(
42
+ response: Response, safe_mode: RequestSafeMode
43
+ ) -> ResponseJson:
44
+ """
45
+ Util to handle a HTTP Response based on the response status code and the
46
+ safe mode used
47
+ """
48
+ try:
49
+ response.raise_for_status()
50
+ except HTTPError as e:
51
+ safe_mode.catch_response(e, response.status_code)
52
+ if safe_mode.should_raise:
53
+ raise e
54
+ logger.error(f"Safe mode : skip request with error {e}")
55
+ logger.debug(e, exc_info=True)
56
+ return {}
57
+ return response.json()
@@ -0,0 +1,77 @@
1
+ import io
2
+ from http import HTTPStatus
3
+
4
+ import pytest
5
+ from requests import HTTPError, Response
6
+
7
+ from .safe_request import RequestSafeMode, handle_response
8
+
9
+
10
+ def mock_response(status_code: int):
11
+ response = Response()
12
+ response.status_code = status_code
13
+ response.raw = io.BytesIO(b'[{"data": "working"}]')
14
+ return response
15
+
16
+
17
+ def test_http_error_with_no_safe_mode():
18
+ safe_params = RequestSafeMode() # Caught
19
+
20
+ with pytest.raises(HTTPError):
21
+ handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
22
+
23
+
24
+ def test_http_error_with_no_status_code():
25
+ safe_params = RequestSafeMode(2) # Caught
26
+
27
+ with pytest.raises(HTTPError):
28
+ handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
29
+
30
+
31
+ def test_http_error_with_status_code():
32
+ safe_params = RequestSafeMode(2, (HTTPStatus.FORBIDDEN,)) # Caught
33
+
34
+ def call():
35
+ return handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
36
+
37
+ assert call() == {}
38
+ assert call() == {}
39
+
40
+ with pytest.raises(HTTPError):
41
+ call()
42
+
43
+
44
+ def test_http_error_with_multiple_status_code():
45
+ safe_params = RequestSafeMode(
46
+ 2, (HTTPStatus.NOT_FOUND, HTTPStatus.FORBIDDEN)
47
+ ) # Caught
48
+
49
+ def call():
50
+ return handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
51
+
52
+ def call_2():
53
+ return handle_response(mock_response(HTTPStatus.NOT_FOUND), safe_params)
54
+
55
+ assert call() == {}
56
+ assert call_2() == {}
57
+ with pytest.raises(HTTPError): # 3 failed calls > retries
58
+ call()
59
+
60
+
61
+ def test_http_error_with_wrong_status_code():
62
+ safe_params = RequestSafeMode(2, (HTTPStatus.NOT_FOUND,)) # Wrong Status
63
+
64
+ def call():
65
+ handle_response(mock_response(HTTPStatus.BAD_REQUEST), safe_params)
66
+
67
+ with pytest.raises(HTTPError):
68
+ call()
69
+
70
+
71
+ def test_http_error_with_return():
72
+ safe_params = RequestSafeMode(2, (HTTPStatus.NOT_FOUND,)) # Wrong Status
73
+
74
+ def call():
75
+ return handle_response(mock_response(HTTPStatus.OK), safe_params)
76
+
77
+ assert call() == [{"data": "working"}]
@@ -1,3 +1,2 @@
1
1
  from .client import SalesforceBaseClient
2
- from .constants import Keys
3
- from .credentials import SalesforceCredentials, to_credentials
2
+ from .credentials import SalesforceCredentials
@@ -1,13 +1,2 @@
1
1
  DEFAULT_API_VERSION = 59.0
2
2
  DEFAULT_PAGINATION_LIMIT = 100
3
-
4
-
5
- class Keys:
6
- """Salesforce's credentials keys"""
7
-
8
- USERNAME = "username"
9
- PASSWORD = "password" # noqa: S105
10
- CLIENT_ID = "client_id"
11
- CLIENT_SECRET = "client_secret" # noqa: S105
12
- SECURITY_TOKEN = "security_token" # noqa: S105
13
- BASE_URL = "base_url"
@@ -1,36 +1,33 @@
1
1
  from typing import Dict
2
2
 
3
- from ...utils import from_env
4
- from .constants import Keys
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
5
 
6
- _USERNAME = "CASTOR_SALESFORCE_USERNAME"
7
- _PASSWORD = "CASTOR_SALESFORCE_PASSWORD" # noqa: S105
8
- _SECURITY_TOKEN = "CASTOR_SALESFORCE_SECURITY_TOKEN" # noqa: S105
9
- _CLIENT_ID = "CASTOR_SALESFORCE_CLIENT_ID"
10
- _CLIENT_SECRET = "CASTOR_SALESFORCE_CLIENT_SECRET" # noqa: S105
11
- _BASE_URL = "CASTOR_SALESFORCE_BASE_URL"
6
+ CASTOR_ENV_PREFIX = "CASTOR_SALESFORCE_"
12
7
 
13
8
 
14
- class SalesforceCredentials:
9
+ class SalesforceCredentials(BaseSettings):
15
10
  """
16
11
  Class to handle Salesforce rest API permissions
17
12
  """
18
13
 
19
- def __init__(
20
- self,
21
- *,
22
- username: str,
23
- password: str,
24
- security_token: str,
25
- client_id: str,
26
- client_secret: str,
27
- base_url: str,
28
- ):
29
- self.username = username
30
- self.password = password + security_token
31
- self.client_id = client_id
32
- self.client_secret = client_secret
33
- self.base_url = base_url
14
+ model_config = SettingsConfigDict(
15
+ env_prefix=CASTOR_ENV_PREFIX,
16
+ extra="ignore",
17
+ populate_by_name=True,
18
+ )
19
+
20
+ base_url: str
21
+ client_id: str
22
+ client_secret: str = Field(repr=False)
23
+ password: str = Field(repr=False)
24
+ security_token: str = Field(repr=False)
25
+ username: str
26
+
27
+ @property
28
+ def password_token(self) -> str:
29
+ """Generates the password for authentication"""
30
+ return self.password + self.security_token
34
31
 
35
32
  def token_request_payload(self) -> Dict[str, str]:
36
33
  """
@@ -41,25 +38,5 @@ class SalesforceCredentials:
41
38
  "client_id": self.client_id,
42
39
  "client_secret": self.client_secret,
43
40
  "username": self.username,
44
- "password": self.password,
41
+ "password": self.password_token,
45
42
  }
46
-
47
-
48
- def to_credentials(params: dict) -> SalesforceCredentials:
49
- """extract Salesforce credentials"""
50
- username = params.get(Keys.USERNAME) or from_env(_USERNAME)
51
- password = params.get(Keys.PASSWORD) or from_env(_PASSWORD)
52
- security_token = params.get(Keys.SECURITY_TOKEN) or from_env(
53
- _SECURITY_TOKEN
54
- )
55
- client_id = params.get(Keys.CLIENT_ID) or from_env(_CLIENT_ID)
56
- client_secret = params.get(Keys.CLIENT_SECRET) or from_env(_CLIENT_SECRET)
57
- base_url = params.get(Keys.BASE_URL) or from_env(_BASE_URL)
58
- return SalesforceCredentials(
59
- username=username,
60
- password=password,
61
- client_id=client_id,
62
- client_secret=client_secret,
63
- security_token=security_token,
64
- base_url=base_url,
65
- )
@@ -1,3 +1,3 @@
1
1
  from .assets import DomoAsset
2
- from .client import CredentialsKey, DomoClient, DomoCredentials
2
+ from .client import DomoClient, DomoCredentials
3
3
  from .extract import extract_all
@@ -1,2 +1,2 @@
1
1
  from .client import DomoClient
2
- from .credentials import CredentialsKey, DomoCredentials
2
+ from .credentials import DomoCredentials
@@ -1,11 +1,21 @@
1
1
  import logging
2
2
  from datetime import datetime, timedelta
3
3
  from http import HTTPStatus
4
- from typing import Iterator, List, Optional, Set, Tuple
4
+ from typing import Iterator, List, Optional, Set
5
5
 
6
6
  import requests
7
7
 
8
- from ....utils import at_midnight, current_date, past_date, retry, timestamp_ms
8
+ from ....utils import (
9
+ RequestSafeMode,
10
+ ResponseJson,
11
+ at_midnight,
12
+ current_date,
13
+ empty_iterator,
14
+ handle_response,
15
+ past_date,
16
+ retry,
17
+ timestamp_ms,
18
+ )
9
19
  from ..assets import DomoAsset
10
20
  from .credentials import DomoCredentials
11
21
  from .endpoints import Endpoint, EndpointFactory
@@ -17,11 +27,17 @@ DOMO_PUBLIC_URL = "https://api.domo.com"
17
27
  DEFAULT_TIMEOUT = 120
18
28
  TOKEN_EXPIRATION_SECONDS = timedelta(seconds=3000) # auth token lasts 1 hour
19
29
 
30
+
31
+ # Safe Mode
32
+ VOLUME_IGNORED = 10
20
33
  IGNORED_ERROR_CODES = (
21
34
  HTTPStatus.BAD_REQUEST,
22
35
  HTTPStatus.NOT_FOUND,
23
36
  )
24
- ERROR_TPL = "Request failed with status code {status_code} and reason {reason}"
37
+ DOMO_SAFE_MODE = RequestSafeMode(
38
+ max_errors=VOLUME_IGNORED,
39
+ status_codes=IGNORED_ERROR_CODES,
40
+ )
25
41
 
26
42
  _RETRY_EXCEPTIONS = [
27
43
  requests.exceptions.ConnectTimeout,
@@ -33,40 +49,17 @@ _RETRY_BASE_MS = 10 * 60 * 1000 # 10 minutes
33
49
  logger = logging.getLogger(__name__)
34
50
 
35
51
 
36
- def _handle_response(response: requests.Response) -> requests.Response:
37
- response.raise_for_status()
38
- return response
39
-
40
-
41
- def _ignore_or_raise(
42
- error: requests.RequestException,
43
- ignore_error_codes: Optional[Tuple[int, ...]],
44
- ) -> dict:
45
- """
46
- Raises the error unless the response status code is in the ignored error
47
- codes list.
48
- """
49
- if not ignore_error_codes:
50
- raise error
51
-
52
- response = error.response
53
- if response is None:
54
- raise error
55
-
56
- if response.status_code in ignore_error_codes:
57
- logger.warning(error)
58
- return {}
59
-
60
- raise error
61
-
62
-
63
52
  class DomoClient:
64
53
  """
65
54
  Connect to Domo API and fetch main assets.
66
55
  https://developer.domo.com/portal/8ba9aedad3679-ap-is#platform-oauth-apis
67
56
  """
68
57
 
69
- def __init__(self, credentials: DomoCredentials):
58
+ def __init__(
59
+ self,
60
+ credentials: DomoCredentials,
61
+ safe_mode: Optional[RequestSafeMode] = None,
62
+ ):
70
63
  self._authentication = credentials.authentication
71
64
  self._bearer_headers: Optional[dict] = None
72
65
  self._session = requests.session()
@@ -76,6 +69,7 @@ class DomoClient:
76
69
  self._timeout = DEFAULT_TIMEOUT
77
70
  self.base_url = credentials.base_url
78
71
  self.cloud_id = credentials.cloud_id
72
+ self.safe_mode = safe_mode or DOMO_SAFE_MODE
79
73
 
80
74
  def _token_expired(self) -> bool:
81
75
  token_lifetime = datetime.now() - self._token_creation_time
@@ -95,7 +89,8 @@ class DomoClient:
95
89
  auth=basic_authentication,
96
90
  timeout=self._timeout,
97
91
  )
98
- result = _handle_response(response).json()
92
+ response.raise_for_status()
93
+ result = response.json()
99
94
 
100
95
  bearer_token = result["access_token"]
101
96
  self._bearer_headers = {"authorization": f"Bearer {bearer_token}"}
@@ -113,7 +108,7 @@ class DomoClient:
113
108
  endpoint: Endpoint,
114
109
  params: Optional[dict] = None,
115
110
  asset_id: Optional[str] = None,
116
- ) -> requests.Response:
111
+ ) -> ResponseJson:
117
112
  params = params if params else {}
118
113
  is_private = endpoint.is_private
119
114
  headers = self._private_headers if is_private else self._bearer_auth()
@@ -125,27 +120,16 @@ class DomoClient:
125
120
  timeout=self._timeout,
126
121
  )
127
122
 
128
- if response.status_code != HTTPStatus.OK:
129
- logger.warning(
130
- ERROR_TPL.format(
131
- status_code=response.status_code,
132
- reason=response.reason,
133
- )
134
- )
135
- return response
123
+ return handle_response(response, self.safe_mode)
136
124
 
137
125
  def _get_element(
138
126
  self,
139
127
  endpoint: Endpoint,
140
128
  params: Optional[dict] = None,
141
129
  asset_id: Optional[str] = None,
142
- ignore_error_codes: Optional[Tuple[int, ...]] = None,
143
130
  ) -> dict:
144
131
  """Used when the response only contains one element"""
145
- try:
146
- return self._get(endpoint, params, asset_id).json()
147
- except requests.RequestException as error:
148
- return _ignore_or_raise(error, ignore_error_codes)
132
+ return self._get(endpoint, params, asset_id)
149
133
 
150
134
  def _get_many(
151
135
  self,
@@ -154,7 +138,7 @@ class DomoClient:
154
138
  asset_id: Optional[str] = None,
155
139
  ) -> List[dict]:
156
140
  """Used when the response contains multiple elements"""
157
- return self._get(endpoint, params, asset_id).json()
141
+ return self._get(endpoint, params, asset_id)
158
142
 
159
143
  def _get_paginated(self, endpoint: Endpoint) -> List[dict]:
160
144
  """Used when the response is paginated and need iterations"""
@@ -172,10 +156,7 @@ class DomoClient:
172
156
 
173
157
  def _datasources(self, page_id: str) -> RawData:
174
158
  endpoint = self._endpoint_factory.page_content(page_id)
175
- page_content = self._get_element(
176
- endpoint,
177
- ignore_error_codes=IGNORED_ERROR_CODES,
178
- )
159
+ page_content = self._get_element(endpoint)
179
160
  processed: set[str] = set()
180
161
  for card in page_content.get("cards", []):
181
162
  for datasource in card["datasources"]:
@@ -196,7 +177,7 @@ class DomoClient:
196
177
  ) -> Iterator[dict]:
197
178
  """Recursively fetch pages while building the folder architecture"""
198
179
  if not page_tree:
199
- return []
180
+ return empty_iterator()
200
181
 
201
182
  for page in page_tree:
202
183
  page_id = page.get("id")
@@ -211,6 +192,10 @@ class DomoClient:
211
192
  self._endpoint_factory.pages,
212
193
  asset_id=page_id,
213
194
  )
195
+
196
+ if not detail:
197
+ continue
198
+
214
199
  datasources = self._datasources(page_id)
215
200
  yield {
216
201
  **detail,
@@ -1,39 +1,26 @@
1
- from dataclasses import dataclass
2
- from enum import Enum
3
- from typing import Dict, Optional, Tuple
1
+ from typing import Dict, Optional
4
2
 
3
+ from pydantic import Field, SecretStr
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
5
  from requests.auth import HTTPBasicAuth
6
6
 
7
+ DOMO_ENV_PREFIX = "CASTOR_DOMO_"
7
8
 
8
- class CredentialsKey(Enum):
9
- """Value enum object for the credentials"""
10
9
 
11
- API_TOKEN = "api_token" # noqa: S105
12
- BASE_URL = "base_url"
13
- CLIENT_ID = "client_id"
14
- CLOUD_ID = "cloud_id"
15
- DEVELOPER_TOKEN = "developer_token" # noqa: S105
16
-
17
-
18
- CLIENT_ALLOWED_KEYS: Tuple[str, ...] = tuple(c.value for c in CredentialsKey)
19
-
20
-
21
- @dataclass
22
- class DomoCredentials:
10
+ class DomoCredentials(BaseSettings):
23
11
  """Class to handle Domo rest API permissions"""
24
12
 
25
- api_token: str
13
+ model_config = SettingsConfigDict(
14
+ env_prefix=DOMO_ENV_PREFIX,
15
+ extra="ignore",
16
+ populate_by_name=True,
17
+ )
18
+
19
+ api_token: str = Field(repr=False)
26
20
  base_url: str
27
21
  client_id: str
28
- developer_token: str
29
- cloud_id: Optional[str] = None
30
-
31
- @classmethod
32
- def from_secret(cls, secret: dict) -> "DomoCredentials":
33
- credentials = {
34
- k: v for k, v in secret.items() if k in CLIENT_ALLOWED_KEYS
35
- }
36
- return cls(**credentials)
22
+ cloud_id: Optional[str] = Field(validation_alias="CLOUD_ID", default=None)
23
+ developer_token: str = Field(repr=False)
37
24
 
38
25
  @property
39
26
  def authentication(self) -> HTTPBasicAuth:
@@ -12,7 +12,6 @@ from ...utils import (
12
12
  )
13
13
  from .assets import DomoAsset
14
14
  from .client import DomoClient, DomoCredentials
15
- from .constants import API_TOKEN, BASE_URL, CLIENT_ID, CLOUD_ID, DEVELOPER_TOKEN
16
15
 
17
16
  logger = logging.getLogger(__name__)
18
17
 
@@ -57,18 +56,13 @@ def extract_all(
57
56
  """
58
57
 
59
58
  _output_directory = output_directory or from_env(OUTPUT_DIR)
60
- _client_id = client_id or from_env(CLIENT_ID)
61
- _base_url = base_url or from_env(BASE_URL)
62
- _api_token = api_token or from_env(API_TOKEN)
63
- _developer_token = developer_token or from_env(DEVELOPER_TOKEN)
64
- _cloud_id = cloud_id or from_env(CLOUD_ID)
65
59
 
66
60
  credentials = DomoCredentials(
67
- base_url=_base_url,
68
- client_id=_client_id,
69
- api_token=_api_token,
70
- developer_token=_developer_token,
71
- cloud_id=_cloud_id,
61
+ base_url=base_url,
62
+ client_id=client_id,
63
+ api_token=api_token,
64
+ developer_token=developer_token,
65
+ cloud_id=cloud_id,
72
66
  )
73
67
  client = DomoClient(credentials=credentials)
74
68
 
@@ -78,4 +72,4 @@ def extract_all(
78
72
  filename = get_output_filename(key.name.lower(), _output_directory, ts)
79
73
  write_json(filename, data)
80
74
 
81
- write_summary(_output_directory, ts, base_url=_base_url)
75
+ write_summary(_output_directory, ts, base_url=credentials.base_url)
@@ -1,3 +1,8 @@
1
- from .api import ApiClient, Credentials, lookml_explore_names
1
+ from .api import (
2
+ ApiClient,
3
+ ExtractionParameters,
4
+ LookerCredentials,
5
+ lookml_explore_names,
6
+ )
2
7
  from .assets import LookerAsset
3
8
  from .extract import extract_all, iterate_all_data
@@ -1,3 +1,4 @@
1
1
  from .client import ApiClient
2
- from .sdk import Credentials
2
+ from .credentials import LookerCredentials
3
+ from .extraction_parameters import ExtractionParameters
3
4
  from .utils import lookml_explore_names