castor-extractor 0.16.6__py3-none-any.whl → 0.16.9__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 (26) hide show
  1. CHANGELOG.md +13 -0
  2. castor_extractor/utils/__init__.py +2 -1
  3. castor_extractor/utils/collection.py +32 -0
  4. castor_extractor/utils/collection_test.py +60 -0
  5. castor_extractor/utils/time.py +9 -1
  6. castor_extractor/utils/time_test.py +8 -1
  7. castor_extractor/visualization/domo/client/client.py +28 -43
  8. castor_extractor/visualization/domo/client/client_test.py +1 -23
  9. castor_extractor/visualization/domo/client/endpoints.py +13 -6
  10. castor_extractor/visualization/domo/client/pagination.py +4 -0
  11. castor_extractor/visualization/looker/api/client.py +21 -17
  12. castor_extractor/visualization/looker/api/sdk.py +10 -58
  13. castor_extractor/visualization/looker/api/utils.py +1 -1
  14. castor_extractor/visualization/looker/extract.py +2 -1
  15. castor_extractor/visualization/looker/multithreading.py +1 -1
  16. castor_extractor/visualization/tableau_revamp/client/client.py +76 -13
  17. castor_extractor/visualization/tableau_revamp/client/gql_queries.py +18 -15
  18. castor_extractor/visualization/tableau_revamp/client/tsc_fields.py +4 -0
  19. castor_extractor/warehouse/databricks/client.py +12 -5
  20. castor_extractor/warehouse/databricks/client_test.py +22 -3
  21. castor_extractor/warehouse/databricks/format.py +5 -1
  22. {castor_extractor-0.16.6.dist-info → castor_extractor-0.16.9.dist-info}/METADATA +6 -3
  23. {castor_extractor-0.16.6.dist-info → castor_extractor-0.16.9.dist-info}/RECORD +26 -25
  24. {castor_extractor-0.16.6.dist-info → castor_extractor-0.16.9.dist-info}/LICENCE +0 -0
  25. {castor_extractor-0.16.6.dist-info → castor_extractor-0.16.9.dist-info}/WHEEL +0 -0
  26. {castor_extractor-0.16.6.dist-info → castor_extractor-0.16.9.dist-info}/entry_points.txt +0 -0
CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.9 - 2024-05-28
4
+
5
+ * Tableau: extract only fields that are necessary
6
+
7
+ ## 0.16.8 - 2024-05-21
8
+
9
+ * Add compatibility with python 3.12
10
+ * Looker: Bump looker-sdk and refactor client
11
+
12
+ ## 0.16.7 - 2024-05-16
13
+
14
+ * Databricks: allow no emails on user
15
+
3
16
  ## 0.16.6 - 2024-05-14
4
17
 
5
18
  * Introducing the revamped connector for Tableau
@@ -5,7 +5,7 @@ from .client import (
5
5
  SqlalchemyClient,
6
6
  uri_encode,
7
7
  )
8
- from .collection import group_by
8
+ from .collection import 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
@@ -32,6 +32,7 @@ from .time import (
32
32
  current_timestamp,
33
33
  date_after,
34
34
  past_date,
35
+ timestamp_ms,
35
36
  )
36
37
  from .type import Callback, Getter, JsonType, SerializedAsset
37
38
  from .validation import validate_baseurl
@@ -12,3 +12,35 @@ def group_by(identifier: Getter, elements: Sequence) -> Dict[Any, List]:
12
12
  groups[key].append(element)
13
13
 
14
14
  return groups
15
+
16
+
17
+ def mapping_from_rows(rows: List[Dict], key: Any, value: Any) -> Dict:
18
+ """
19
+ Create a dictionary mapping from a list of dictionaries using specified keys for mapping.
20
+
21
+ Args:
22
+ rows (list[dict]): A list of dictionaries from which to create the mapping.
23
+ key (Any): The key to use for the keys of the resulting dictionary.
24
+ value (Any): The key to use for the values of the resulting dictionary.
25
+
26
+ Returns:
27
+ dict: A dictionary where each key-value pair corresponds to the specified key and value
28
+ from each dictionary in the input list. Only dictionaries with both specified key
29
+ and value present are included in the result.
30
+
31
+ Example:
32
+ rows = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
33
+ mapping = mapping_from_rows(rows, 'id', 'name')
34
+ # mapping will be {1: 'Alice', 2: 'Bob'}
35
+ """
36
+ mapping = {}
37
+
38
+ for row in rows:
39
+ mapping_key = row.get(key)
40
+ mapping_value = row.get(value)
41
+
42
+ if not mapping_key or not mapping_value:
43
+ continue
44
+ mapping[mapping_key] = mapping_value
45
+
46
+ return mapping
@@ -0,0 +1,60 @@
1
+ import pytest
2
+
3
+ from .collection import mapping_from_rows
4
+
5
+
6
+ def test__mapping_from_rows__basic_mapping():
7
+ rows = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
8
+ result = mapping_from_rows(rows, "id", "name")
9
+ expected = {1: "Alice", 2: "Bob"}
10
+ assert result == expected
11
+
12
+
13
+ def test__mapping_from_rows__missing_key():
14
+ rows = [{"id": 1, "name": "Alice"}, {"name": "Bob"}]
15
+ result = mapping_from_rows(rows, "id", "name")
16
+ expected = {1: "Alice"}
17
+ assert result == expected
18
+
19
+
20
+ def test__mapping_from_rows__missing_value():
21
+ rows = [{"id": 1, "name": "Alice"}, {"id": 2}]
22
+ result = mapping_from_rows(rows, "id", "name")
23
+ expected = {1: "Alice"}
24
+ assert result == expected
25
+
26
+
27
+ def test__mapping_from_rows__empty_list():
28
+ rows = []
29
+ result = mapping_from_rows(rows, "id", "name")
30
+ expected = {}
31
+ assert result == expected
32
+
33
+
34
+ def test__mapping_from_rows__non_existent_key_value():
35
+ rows = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
36
+ result = mapping_from_rows(rows, "nonexistent_key", "nonexistent_value")
37
+ expected = {}
38
+ assert result == expected
39
+
40
+
41
+ def test__mapping_from_rows__none_key_value():
42
+ rows = [
43
+ {"id": 1, "name": "Alice"},
44
+ {"id": None, "name": "Bob"},
45
+ {"id": 2, "name": None},
46
+ ]
47
+ result = mapping_from_rows(rows, "id", "name")
48
+ expected = {1: "Alice"}
49
+ assert result == expected
50
+
51
+
52
+ def test__mapping_from_rows__multiple_valid_rows():
53
+ rows = [
54
+ {"id": 1, "name": "Alice"},
55
+ {"id": 2, "name": "Bob"},
56
+ {"id": 3, "name": "Charlie"},
57
+ ]
58
+ result = mapping_from_rows(rows, "id", "name")
59
+ expected = {1: "Alice", 2: "Bob", 3: "Charlie"}
60
+ assert result == expected
@@ -15,7 +15,7 @@ def current_timestamp() -> int:
15
15
  """
16
16
  Returns the current timestamp from epoch (rounded to the nearest second)
17
17
  """
18
- return int(datetime.timestamp(current_datetime()))
18
+ return int(current_datetime().timestamp())
19
19
 
20
20
 
21
21
  def _set_uct_timezone(ts: datetime) -> datetime:
@@ -24,6 +24,14 @@ def _set_uct_timezone(ts: datetime) -> datetime:
24
24
  return ts
25
25
 
26
26
 
27
+ def timestamp_ms(ts: datetime) -> int:
28
+ """
29
+ Return ts timestamp in millisecond (rounded)
30
+ """
31
+ ts_utc = _set_uct_timezone(ts)
32
+ return int(ts_utc.timestamp() * 1000)
33
+
34
+
27
35
  def now(tz: bool = False) -> datetime:
28
36
  """
29
37
  provide current time
@@ -1,6 +1,6 @@
1
1
  from datetime import date, datetime
2
2
 
3
- from .time import at_midnight, date_after
3
+ from .time import at_midnight, date_after, timestamp_ms
4
4
 
5
5
 
6
6
  def test_at_midnight():
@@ -10,3 +10,10 @@ def test_at_midnight():
10
10
  def test_date_after():
11
11
  day = date(1999, 12, 31)
12
12
  assert date_after(day, 3) == date(2000, 1, 3)
13
+
14
+
15
+ def test_timestamp_ms():
16
+ dt = datetime(1991, 4, 3, 0, 0)
17
+ result = timestamp_ms(dt)
18
+ expected = 670636800000
19
+ assert result == expected
@@ -1,10 +1,11 @@
1
1
  import logging
2
- from datetime import date, datetime, timedelta
2
+ from datetime import datetime, timedelta
3
3
  from http import HTTPStatus
4
4
  from typing import Iterator, List, Optional, Set, Tuple
5
5
 
6
6
  import requests
7
7
 
8
+ from ....utils import at_midnight, current_date, past_date, retry, timestamp_ms
8
9
  from ..assets import DomoAsset
9
10
  from .credentials import DomoCredentials
10
11
  from .endpoints import Endpoint, EndpointFactory
@@ -13,8 +14,7 @@ from .pagination import Pagination
13
14
  RawData = Iterator[dict]
14
15
 
15
16
  DOMO_PUBLIC_URL = "https://api.domo.com"
16
- FORMAT = "%Y-%m-%d %I:%M:%S %p"
17
- DEFAULT_TIMEOUT = 500
17
+ DEFAULT_TIMEOUT = 120
18
18
  TOKEN_EXPIRATION_SECONDS = timedelta(seconds=3000) # auth token lasts 1 hour
19
19
 
20
20
  IGNORED_ERROR_CODES = (
@@ -23,12 +23,14 @@ IGNORED_ERROR_CODES = (
23
23
  )
24
24
  ERROR_TPL = "Request failed with status code {status_code} and reason {reason}"
25
25
 
26
- logger = logging.getLogger(__name__)
27
-
26
+ _RETRY_EXCEPTIONS = [
27
+ requests.exceptions.ConnectTimeout,
28
+ requests.exceptions.ReadTimeout,
29
+ ]
30
+ _RETRY_COUNT = 2
31
+ _RETRY_BASE_MS = 10 * 60 * 1000 # 10 minutes
28
32
 
29
- def _at_midnight(date_: date) -> datetime:
30
- """convert date into datetime at midnight: 00:00:00"""
31
- return datetime.combine(date_, datetime.min.time())
33
+ logger = logging.getLogger(__name__)
32
34
 
33
35
 
34
36
  def _handle_response(response: requests.Response) -> requests.Response:
@@ -36,21 +38,6 @@ def _handle_response(response: requests.Response) -> requests.Response:
36
38
  return response
37
39
 
38
40
 
39
- def _is_expired(last_result: dict) -> bool:
40
- """
41
- Checks if the date given is expired
42
- i.e. date < yesterday (00:00 AM)
43
- """
44
-
45
- last_timestamp = datetime.strptime(last_result["time"], FORMAT)
46
- logger.info(f"Last audit input has timestamp: {last_timestamp}")
47
- yesterday = date.today() - timedelta(days=1)
48
- threshold = _at_midnight(yesterday)
49
- if last_timestamp < threshold:
50
- return True
51
- return False
52
-
53
-
54
41
  def _ignore_or_raise(
55
42
  error: requests.RequestException,
56
43
  ignore_error_codes: Optional[Tuple[int, ...]],
@@ -82,6 +69,7 @@ class DomoClient:
82
69
  def __init__(self, credentials: DomoCredentials):
83
70
  self._authentication = credentials.authentication
84
71
  self._bearer_headers: Optional[dict] = None
72
+ self._session = requests.session()
85
73
  self._token_creation_time: datetime = datetime.min
86
74
  self._endpoint_factory = EndpointFactory(credentials.base_url)
87
75
  self._private_headers = credentials.private_headers
@@ -102,7 +90,7 @@ class DomoClient:
102
90
  basic_authentication = self._authentication
103
91
  endpoint = self._endpoint_factory.authentication
104
92
 
105
- response = requests.get(
93
+ response = self._session.get(
106
94
  endpoint.url(),
107
95
  auth=basic_authentication,
108
96
  timeout=self._timeout,
@@ -115,6 +103,11 @@ class DomoClient:
115
103
 
116
104
  return self._bearer_headers
117
105
 
106
+ @retry(
107
+ exceptions=_RETRY_EXCEPTIONS,
108
+ max_retries=_RETRY_COUNT,
109
+ base_ms=_RETRY_BASE_MS,
110
+ )
118
111
  def _get(
119
112
  self,
120
113
  endpoint: Endpoint,
@@ -124,12 +117,14 @@ class DomoClient:
124
117
  params = params if params else {}
125
118
  is_private = endpoint.is_private
126
119
  headers = self._private_headers if is_private else self._bearer_auth()
127
- response = requests.get(
120
+
121
+ response = self._session.get(
128
122
  url=endpoint.url(asset_id),
129
123
  headers=headers,
130
124
  params=params,
131
125
  timeout=self._timeout,
132
126
  )
127
+
133
128
  if response.status_code != HTTPStatus.OK:
134
129
  logger.warning(
135
130
  ERROR_TPL.format(
@@ -165,28 +160,14 @@ class DomoClient:
165
160
  """Used when the response is paginated and need iterations"""
166
161
  pagination = Pagination()
167
162
  all_results: List[dict] = []
163
+
168
164
  while pagination.needs_increment:
169
- results = self._get_many(
170
- endpoint=endpoint,
171
- params={
172
- "offset": pagination.offset,
173
- "limit": pagination.per_page,
174
- },
175
- )
165
+ params = {**pagination.params, **endpoint.params}
166
+ results = self._get_many(endpoint=endpoint, params=params)
176
167
  all_results.extend(results)
177
168
  number_of_items = len(results)
178
169
  pagination.increment_offset(number_of_items)
179
170
 
180
- if endpoint == self._endpoint_factory.audit:
181
- last_result = results[-1]
182
- # We decided to fetch audits until the audit is prior to a particular date
183
- # defined in _is_expired function. To be more precise, we could have instead use
184
- # params start and end of the audit endpoint (timestamp in milliseconds).
185
- # https://developer.domo.com/portal/a4e18ca6a0c0b-retrieve-activity-log-entries
186
- # But we need then to manage properly the timezone conversion of the results
187
- # which adds more complexity than benefits
188
- if _is_expired(last_result):
189
- pagination.should_stop = True
190
171
  return all_results
191
172
 
192
173
  def _datasources(self, page_id: str) -> RawData:
@@ -300,7 +281,11 @@ class DomoClient:
300
281
  )
301
282
 
302
283
  def _audit(self) -> RawData:
303
- yield from self._get_paginated(self._endpoint_factory.audit)
284
+ yesterday = timestamp_ms(at_midnight(past_date(1)))
285
+ today = timestamp_ms(at_midnight(current_date()))
286
+ yield from self._get_paginated(
287
+ self._endpoint_factory.audit(yesterday, today)
288
+ )
304
289
 
305
290
  def _dataflows(self) -> RawData:
306
291
  dataflows = self._get_many(self._endpoint_factory.dataflows)
@@ -1,33 +1,11 @@
1
- from datetime import date, datetime, timedelta
2
1
  from unittest.mock import patch
3
2
 
4
3
  import pytest
5
4
  import requests
6
5
 
7
- from .client import (
8
- FORMAT,
9
- DomoClient,
10
- DomoCredentials,
11
- _at_midnight,
12
- _is_expired,
13
- )
6
+ from .client import DomoClient, DomoCredentials
14
7
  from .endpoints import EndpointFactory
15
8
 
16
- NEVER_EXPIRING_DATE = datetime.max
17
- TODAY_DATE = _at_midnight(date.today())
18
- EXPIRED_DATE = _at_midnight(date.today() - timedelta(days=2))
19
-
20
-
21
- def test__is_expired():
22
- test_datetimes = (
23
- ({"time": NEVER_EXPIRING_DATE.strftime(FORMAT)}, False),
24
- ({"time": TODAY_DATE.strftime(FORMAT)}, False),
25
- ({"time": EXPIRED_DATE.strftime(FORMAT)}, True),
26
- )
27
-
28
- for test_datetime, is_expired in test_datetimes:
29
- assert _is_expired(test_datetime) == is_expired
30
-
31
9
 
32
10
  class FakeResponse:
33
11
  """
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from typing import Optional
3
3
 
4
4
  _DOMO_PUBLIC_URL = "https://api.domo.com"
@@ -13,6 +13,7 @@ class Endpoint:
13
13
 
14
14
  base_url: str
15
15
  is_private: bool = False
16
+ params: dict = field(default_factory=dict)
16
17
 
17
18
  def url(self, asset_id: Optional[str] = None):
18
19
  return f"{self.base_url}/{asset_id}" if asset_id else self.base_url
@@ -40,10 +41,6 @@ class EndpointFactory:
40
41
  def users(self) -> Endpoint:
41
42
  return Endpoint(f"{_DOMO_PUBLIC_URL}/v1/users")
42
43
 
43
- @property
44
- def audit(self) -> Endpoint:
45
- return Endpoint(f"{_DOMO_PUBLIC_URL}/v1/audit")
46
-
47
44
  @property
48
45
  def dataflows(self) -> Endpoint:
49
46
  return Endpoint(
@@ -51,6 +48,16 @@ class EndpointFactory:
51
48
  is_private=True,
52
49
  )
53
50
 
51
+ def audit(self, start: int, end: int) -> Endpoint:
52
+ """
53
+ start and end are timestamps since epoch in milliseconds
54
+ See [documentation](https://developer.domo.com/portal/a4e18ca6a0c0b-retrieve-activity-log-entries)
55
+ """
56
+ return Endpoint(
57
+ base_url=f"{_DOMO_PUBLIC_URL}/v1/audit",
58
+ params={"start": start, "end": end},
59
+ )
60
+
54
61
  def lineage(self, dataflow_id: str) -> Endpoint:
55
62
  return Endpoint(
56
63
  base_url=f"{self.base_url}/api/data/v1/lineage/DATAFLOW/{dataflow_id}",
@@ -65,6 +72,6 @@ class EndpointFactory:
65
72
 
66
73
  def page_content(self, page_id: str) -> Endpoint:
67
74
  return Endpoint(
68
- f"{self.base_url}/api/content/v3/stacks/{page_id}/cards?parts=datasources",
75
+ base_url=f"{self.base_url}/api/content/v3/stacks/{page_id}/cards?parts=datasources",
69
76
  is_private=True,
70
77
  )
@@ -23,6 +23,10 @@ class Pagination:
23
23
 
24
24
  return True
25
25
 
26
+ @property
27
+ def params(self) -> dict:
28
+ return {"offset": self.offset, "limit": self.per_page}
29
+
26
30
  def increment_offset(self, number_results: int) -> None:
27
31
  self.offset += number_results
28
32
  self.number_results = number_results
@@ -4,7 +4,22 @@ from datetime import date, timedelta
4
4
  from typing import Callable, Iterator, List, Optional, Sequence, Tuple
5
5
 
6
6
  from dateutil.utils import today
7
- from looker_sdk.sdk.api40.models import ContentView, UserAttribute
7
+ from looker_sdk import init40
8
+ from looker_sdk.sdk.api40.models import (
9
+ ContentView,
10
+ Dashboard,
11
+ DBConnection,
12
+ Folder,
13
+ GroupHierarchy,
14
+ GroupSearch,
15
+ Look,
16
+ LookmlModel,
17
+ LookmlModelExplore,
18
+ Project,
19
+ User,
20
+ UserAttribute,
21
+ )
22
+ from looker_sdk.sdk.constants import sdk_version
8
23
 
9
24
  from ....utils import Pager, PagerLogger, SafeMode, past_date, safe_mode
10
25
  from ..env import page_size
@@ -24,21 +39,7 @@ from .constants import (
24
39
  USER_FIELDS,
25
40
  USERS_ATTRIBUTES_FIELDS,
26
41
  )
27
- from .sdk import (
28
- Credentials,
29
- Dashboard,
30
- DBConnection,
31
- Folder,
32
- GroupHierarchy,
33
- GroupSearch,
34
- Look,
35
- LookmlModel,
36
- LookmlModelExplore,
37
- Project,
38
- User,
39
- has_admin_permissions,
40
- init40,
41
- )
42
+ from .sdk import CastorApiSettings, Credentials, has_admin_permissions
42
43
 
43
44
  logger = logging.getLogger(__name__)
44
45
 
@@ -81,7 +82,10 @@ class ApiClient:
81
82
  on_api_call: OnApiCall = lambda: None,
82
83
  safe_mode: Optional[SafeMode] = None,
83
84
  ):
84
- sdk = init40(credentials)
85
+ settings = CastorApiSettings(
86
+ credentials=credentials, sdk_version=sdk_version
87
+ )
88
+ sdk = init40(config_settings=settings)
85
89
  if not has_admin_permissions(sdk):
86
90
  raise PermissionError("User does not have admin access.")
87
91
  else:
@@ -4,35 +4,9 @@ This file is a proxy to Looker SDK functions using Castor ApiSettings.
4
4
 
5
5
  from typing import Optional
6
6
 
7
- from looker_sdk.error import SDKError
8
- from looker_sdk.rtl import (
9
- auth_session,
10
- requests_transport,
11
- serialize,
12
- transport,
13
- )
14
- from looker_sdk.rtl.api_settings import PApiSettings, SettingsConfig
15
- from looker_sdk.sdk import constants
7
+ from looker_sdk.rtl import transport
8
+ from looker_sdk.rtl.api_settings import ApiSettings, SettingsConfig
16
9
  from looker_sdk.sdk.api40 import methods as methods40
17
- from looker_sdk.sdk.api40.models import (
18
- Dashboard,
19
- DashboardElement,
20
- DBConnection,
21
- Folder,
22
- GroupHierarchy,
23
- GroupSearch,
24
- Look,
25
- LookmlModel,
26
- LookmlModelExplore,
27
- LookmlModelExploreField,
28
- LookmlModelExploreFieldset,
29
- LookmlModelExploreJoins,
30
- LookmlModelNavExplore,
31
- Project,
32
- Query,
33
- User,
34
- )
35
- from typing_extensions import Protocol
36
10
 
37
11
  from ..env import timeout_second
38
12
 
@@ -76,46 +50,24 @@ def has_admin_permissions(sdk_: methods40.Looker40SDK) -> bool:
76
50
  )
77
51
 
78
52
 
79
- def init40(config: Credentials) -> methods40.Looker40SDK:
80
- """Default dependency configuration"""
81
- settings = ApiSettings(config=config, sdk_version=constants.sdk_version)
82
- settings.is_configured()
83
- transport = requests_transport.RequestsTransport.configure(settings)
84
- return methods40.Looker40SDK(
85
- auth=auth_session.AuthSession(
86
- settings,
87
- transport,
88
- serialize.deserialize40,
89
- "4.0",
90
- ),
91
- deserialize=serialize.deserialize40,
92
- serialize=serialize.serialize40,
93
- transport=transport,
94
- api_version="4.0",
95
- )
96
-
97
-
98
- class CastorApiSettings(PApiSettings):
99
- """This is an intermediate class that is meant to be extended (for typing purpose)"""
100
-
101
- def read_config(self) -> SettingsConfig:
102
- raise NotImplementedError()
103
-
104
-
105
- class ApiSettings(CastorApiSettings):
53
+ class CastorApiSettings(ApiSettings):
106
54
  """SDK settings with initialisation using a credential object instead of a path to a .ini file"""
107
55
 
108
- def __init__(self, config: Credentials, sdk_version: Optional[str] = ""):
56
+ def __init__(
57
+ self, credentials: Credentials, sdk_version: Optional[str] = ""
58
+ ):
109
59
  """Configure using a config dict"""
110
- self.config = config.to_settings_config()
60
+ self.config = credentials.to_settings_config()
111
61
  self.verify_ssl = True
112
62
  self.base_url = self.config.get("base_url", "")
113
- self.timeout = config.timeout
63
+ self.timeout = credentials.timeout
114
64
  self.headers = {"Content-Type": "application/json"}
115
65
  self.agent_tag = f"{transport.AGENT_PREFIX}"
116
66
  if sdk_version:
117
67
  self.agent_tag += f" {sdk_version}"
118
68
 
69
+ super().__init__()
70
+
119
71
  def read_config(self) -> SettingsConfig:
120
72
  """Returns a serialization of the credentials"""
121
73
  return self.config
@@ -1,6 +1,6 @@
1
1
  from typing import Iterable, Set, Tuple
2
2
 
3
- from .sdk import LookmlModel
3
+ from looker_sdk.sdk.api40.models import LookmlModel
4
4
 
5
5
 
6
6
  def lookml_explore_names(
@@ -1,6 +1,8 @@
1
1
  import logging
2
2
  from typing import Iterable, Optional, Set, Tuple, Union
3
3
 
4
+ from looker_sdk.sdk.api40.models import LookmlModel
5
+
4
6
  from ...logger import add_logging_file_handler, set_stream_handler_to_stdout
5
7
  from ...utils import (
6
8
  SafeMode,
@@ -12,7 +14,6 @@ from ...utils import (
12
14
  write_summary,
13
15
  )
14
16
  from .api import ApiClient, Credentials, lookml_explore_names
15
- from .api.sdk import LookmlModel
16
17
  from .assets import LookerAsset
17
18
  from .multithreading import MultithreadingFetcher
18
19
  from .parameters import get_parameters
@@ -4,11 +4,11 @@ from concurrent.futures import ThreadPoolExecutor
4
4
  from functools import partial
5
5
  from typing import Iterable, List, Set
6
6
 
7
+ from looker_sdk.error import SDKError
7
8
  from tqdm import tqdm # type: ignore
8
9
 
9
10
  from ...utils import RetryStrategy, deep_serialize, retry
10
11
  from . import ApiClient
11
- from .api.sdk import SDKError
12
12
  from .assets import LookerAsset
13
13
 
14
14
  logger = logging.getLogger(__name__)
@@ -2,6 +2,7 @@ import logging
2
2
  from typing import Dict, Iterator, List, Optional
3
3
 
4
4
  import tableauserverclient as TSC # type: ignore
5
+ from tableauserverclient import Pager
5
6
 
6
7
  from ....utils import SerializedAsset
7
8
  from ..assets import TableauRevampAsset
@@ -12,7 +13,7 @@ from ..constants import (
12
13
  )
13
14
  from .credentials import TableauRevampCredentials
14
15
  from .errors import TableauApiError
15
- from .gql_queries import GQL_QUERIES, QUERY_TEMPLATE
16
+ from .gql_queries import FIELDS_QUERIES, GQL_QUERIES, QUERY_TEMPLATE
16
17
  from .tsc_fields import TSC_FIELDS
17
18
 
18
19
  logger = logging.getLogger(__name__)
@@ -27,13 +28,15 @@ _TSC_ASSETS = (
27
28
  TableauRevampAsset.USAGE,
28
29
  )
29
30
 
31
+ # speed up extraction: fields and columns are smaller but volumes are bigger
30
32
  _CUSTOM_PAGE_SIZE: Dict[TableauRevampAsset, int] = {
31
33
  TableauRevampAsset.FIELD: 1000,
34
+ TableauRevampAsset.COLUMN: 1000,
32
35
  }
33
36
 
34
37
 
35
38
  def _pick_fields(
36
- data: SerializedAsset,
39
+ data: Pager,
37
40
  asset: TableauRevampAsset,
38
41
  ) -> SerializedAsset:
39
42
  fields = TSC_FIELDS[asset]
@@ -44,7 +47,7 @@ def _pick_fields(
44
47
  return [_pick(row) for row in data]
45
48
 
46
49
 
47
- def _enrich_with_tsc(
50
+ def _enrich_datasources_with_tsc(
48
51
  datasources: SerializedAsset,
49
52
  tsc_datasources: SerializedAsset,
50
53
  ) -> SerializedAsset:
@@ -69,6 +72,32 @@ def _enrich_with_tsc(
69
72
  return datasources
70
73
 
71
74
 
75
+ def _enrich_workbooks_with_tsc(
76
+ workbooks: SerializedAsset,
77
+ tsc_workbooks: SerializedAsset,
78
+ ) -> SerializedAsset:
79
+ """
80
+ Enrich workbooks with fields coming from TableauServerClient:
81
+ - project_luid
82
+ """
83
+
84
+ mapping = {row["id"]: row for row in tsc_workbooks}
85
+
86
+ for workbook in workbooks:
87
+ luid = workbook["luid"]
88
+ tsc_workbook = mapping.get(luid)
89
+ if not tsc_workbook:
90
+ # it happens that a workbook is in Metadata API but not in TSC
91
+ # in this case, we push the workbook with default project
92
+ logger.warning(f"Workbook {luid} was not found in TSC")
93
+ workbook["projectLuid"] = None
94
+ continue
95
+
96
+ workbook["projectLuid"] = tsc_workbook["project_id"]
97
+
98
+ return workbooks
99
+
100
+
72
101
  def gql_query_scroll(
73
102
  server,
74
103
  query: str,
@@ -176,29 +205,32 @@ class TableauRevampClient:
176
205
  asset: TableauRevampAsset,
177
206
  ) -> SerializedAsset:
178
207
 
179
- if asset == TableauRevampAsset.USER:
180
- data = TSC.Pager(self._server.users)
208
+ if asset == TableauRevampAsset.DATASOURCE:
209
+ data = TSC.Pager(self._server.datasources)
181
210
 
182
211
  elif asset == TableauRevampAsset.PROJECT:
183
212
  data = TSC.Pager(self._server.projects)
184
213
 
185
- elif asset == TableauRevampAsset.DATASOURCE:
186
- data = TSC.Pager(self._server.datasources)
187
-
188
214
  elif asset == TableauRevampAsset.USAGE:
189
215
  data = TSC.Pager(self._server.views, usage=True)
190
216
 
217
+ elif asset == TableauRevampAsset.USER:
218
+ data = TSC.Pager(self._server.users)
219
+
220
+ elif asset == TableauRevampAsset.WORKBOOK:
221
+ data = TSC.Pager(self._server.workbooks)
222
+
191
223
  else:
192
224
  raise AssertionError(f"Fetching from TSC not supported for {asset}")
193
225
 
194
226
  return _pick_fields(data, asset)
195
227
 
196
- def _fetch_from_metadata_api(
228
+ def _run_graphql_query(
197
229
  self,
198
- asset: TableauRevampAsset,
230
+ resource: str,
231
+ fields: str,
232
+ page_size: int = DEFAULT_PAGE_SIZE,
199
233
  ) -> SerializedAsset:
200
- resource, fields = GQL_QUERIES[asset]
201
- page_size = _CUSTOM_PAGE_SIZE.get(asset) or DEFAULT_PAGE_SIZE
202
234
  query = QUERY_TEMPLATE.format(
203
235
  resource=resource,
204
236
  fields=fields,
@@ -207,13 +239,40 @@ class TableauRevampClient:
207
239
  result_pages = gql_query_scroll(self._server, query, resource)
208
240
  return [asset for page in result_pages for asset in page]
209
241
 
242
+ def _fetch_fields(self) -> SerializedAsset:
243
+ result: SerializedAsset = []
244
+ page_size = _CUSTOM_PAGE_SIZE[TableauRevampAsset.FIELD]
245
+ for resource, fields in FIELDS_QUERIES:
246
+ current = self._run_graphql_query(resource, fields, page_size)
247
+ result.extend(current)
248
+ return result
249
+
250
+ def _fetch_from_metadata_api(
251
+ self,
252
+ asset: TableauRevampAsset,
253
+ ) -> SerializedAsset:
254
+ if asset == TableauRevampAsset.FIELD:
255
+ return self._fetch_fields()
256
+
257
+ page_size = _CUSTOM_PAGE_SIZE.get(asset) or DEFAULT_PAGE_SIZE
258
+ resource, fields = GQL_QUERIES[asset]
259
+ return self._run_graphql_query(resource, fields, page_size)
260
+
210
261
  def _fetch_datasources(self) -> SerializedAsset:
211
262
  asset = TableauRevampAsset.DATASOURCE
212
263
 
213
264
  datasources = self._fetch_from_metadata_api(asset)
214
265
  datasource_projects = self._fetch_from_tsc(asset)
215
266
 
216
- return _enrich_with_tsc(datasources, datasource_projects)
267
+ return _enrich_datasources_with_tsc(datasources, datasource_projects)
268
+
269
+ def _fetch_workbooks(self) -> SerializedAsset:
270
+ asset = TableauRevampAsset.WORKBOOK
271
+
272
+ workbooks = self._fetch_from_metadata_api(asset)
273
+ workbook_projects = self._fetch_from_tsc(asset)
274
+
275
+ return _enrich_workbooks_with_tsc(workbooks, workbook_projects)
217
276
 
218
277
  def fetch(
219
278
  self,
@@ -226,6 +285,10 @@ class TableauRevampClient:
226
285
  # both APIs are required to extract datasources
227
286
  return self._fetch_datasources()
228
287
 
288
+ if asset == TableauRevampAsset.WORKBOOK:
289
+ # both APIs are required to extract workbooks
290
+ return self._fetch_workbooks()
291
+
229
292
  if asset in _TSC_ASSETS:
230
293
  # some assets can only be extracted via TSC
231
294
  return self._fetch_from_tsc(asset)
@@ -37,12 +37,10 @@ workbook { id }
37
37
 
38
38
  _DATASOURCES_QUERY = """
39
39
  __typename
40
- createdAt
41
40
  downstreamDashboards { id }
42
41
  downstreamWorkbooks { id }
43
42
  id
44
43
  name
45
- updatedAt
46
44
  ... on PublishedDatasource {
47
45
  description
48
46
  luid
@@ -64,7 +62,6 @@ name
64
62
  connectionType
65
63
  fullName
66
64
  schema
67
- tableType
68
65
  }
69
66
  ... on CustomSQLTable {
70
67
  query
@@ -80,7 +77,6 @@ id
80
77
  luid
81
78
  name
82
79
  owner { luid }
83
- projectLuid
84
80
  site { name }
85
81
  tags { name }
86
82
  updatedAt
@@ -96,16 +92,17 @@ downstreamWorkbooks { id }
96
92
  folderName
97
93
  id
98
94
  name
99
- ... on DataField {
100
- dataType
101
- role
102
- }
103
- ... on ColumnField {
104
- columns {
105
- name
106
- table { name }
107
- }
108
- }
95
+ dataType
96
+ role
97
+ """
98
+
99
+
100
+ _FIELDS_QUERY_WITH_COLUMNS = f"""
101
+ {_FIELDS_QUERY}
102
+ columns {{
103
+ name
104
+ table {{ name }}
105
+ }}
109
106
  """
110
107
 
111
108
  _SHEETS_QUERY = """
@@ -124,8 +121,14 @@ GQL_QUERIES: Dict[TableauRevampAsset, Tuple[str, str]] = {
124
121
  TableauRevampAsset.COLUMN: ("columns", _COLUMNS_QUERY),
125
122
  TableauRevampAsset.DASHBOARD: ("dashboards", _DASHBOARDS_QUERY),
126
123
  TableauRevampAsset.DATASOURCE: ("datasources", _DATASOURCES_QUERY),
127
- TableauRevampAsset.FIELD: ("fields", _FIELDS_QUERY),
128
124
  TableauRevampAsset.SHEET: ("sheets", _SHEETS_QUERY),
129
125
  TableauRevampAsset.TABLE: ("tables", _TABLES_QUERY),
130
126
  TableauRevampAsset.WORKBOOK: ("workbooks", _WORKBOOKS_QUERY),
131
127
  }
128
+
129
+ FIELDS_QUERIES = (
130
+ ("binFields", _FIELDS_QUERY),
131
+ ("calculatedFields", _FIELDS_QUERY),
132
+ ("columnFields", _FIELDS_QUERY_WITH_COLUMNS),
133
+ ("groupFields", _FIELDS_QUERY),
134
+ )
@@ -27,4 +27,8 @@ TSC_FIELDS: Dict[TableauRevampAsset, Set[str]] = {
27
27
  "name",
28
28
  "site_role",
29
29
  },
30
+ TableauRevampAsset.WORKBOOK: {
31
+ "id",
32
+ "project_id",
33
+ },
30
34
  }
@@ -3,7 +3,7 @@ from datetime import date
3
3
  from functools import partial
4
4
  from typing import Any, Dict, List, Optional, Set
5
5
 
6
- from ...utils import at_midnight, date_after
6
+ from ...utils import at_midnight, date_after, mapping_from_rows
7
7
  from ...utils.client.api import APIClient
8
8
  from ...utils.pager import PagerOnToken
9
9
  from ..abstract.time_filter import TimeFilter
@@ -88,15 +88,22 @@ class DatabricksClient(APIClient):
88
88
  )
89
89
 
90
90
  @staticmethod
91
- def _match_table_with_user(table: dict, user_id_by_email: dict) -> dict:
91
+ def _match_table_with_user(table: dict, user_mapping: dict) -> dict:
92
92
  table_owner_email = table.get("owner_email")
93
93
  if not table_owner_email:
94
94
  return table
95
- owner_external_id = user_id_by_email.get(table_owner_email)
95
+ owner_external_id = user_mapping.get(table_owner_email)
96
96
  if not owner_external_id:
97
97
  return table
98
98
  return {**table, "owner_external_id": owner_external_id}
99
99
 
100
+ @staticmethod
101
+ def _get_user_mapping(users: List[dict]) -> dict:
102
+ return {
103
+ **mapping_from_rows(users, "email", "id"),
104
+ **mapping_from_rows(users, "user_name", "id"),
105
+ }
106
+
100
107
  def tables_and_columns(
101
108
  self, schemas: List[dict], users: List[dict]
102
109
  ) -> TablesColumns:
@@ -105,11 +112,11 @@ class DatabricksClient(APIClient):
105
112
  """
106
113
  tables: List[dict] = []
107
114
  columns: List[dict] = []
108
- user_id_by_email = {user.get("email"): user.get("id") for user in users}
115
+ user_mapping = self._get_user_mapping(users)
109
116
  for schema in schemas:
110
117
  t_to_add, c_to_add = self._tables_columns_of_schema(schema)
111
118
  t_with_owner = [
112
- self._match_table_with_user(table, user_id_by_email)
119
+ self._match_table_with_user(table, user_mapping)
113
120
  for table in t_to_add
114
121
  ]
115
122
  tables.extend(t_with_owner)
@@ -66,15 +66,34 @@ def test_DatabricksClient__keep_catalog():
66
66
  assert not client._keep_catalog("something_unknown")
67
67
 
68
68
 
69
+ def test_DatabricksClient__get_user_mapping():
70
+ client = MockDatabricksClient()
71
+ users = [
72
+ {"id": "both", "email": "hello@world.com", "user_name": "hello world"},
73
+ {"id": "no_email", "email": "", "user_name": "no email"},
74
+ {"id": "no_name", "email": "no@name.fr", "user_name": ""},
75
+ {"id": "no_both", "email": "", "user_name": ""},
76
+ {"id": "", "email": "no@id.com", "user_name": "no id"},
77
+ ]
78
+ expected = {
79
+ "hello@world.com": "both",
80
+ "hello world": "both",
81
+ "no@name.fr": "no_name",
82
+ "no email": "no_email",
83
+ }
84
+ mapping = client._get_user_mapping(users)
85
+ assert mapping == expected
86
+
87
+
69
88
  def test_DatabricksClient__match_table_with_user():
70
89
  client = MockDatabricksClient()
71
- users_by_email = {"bob@castordoc.com": 3}
90
+ user_mapping = {"bob@castordoc.com": 3}
72
91
 
73
92
  table = {"id": 1, "owner_email": "bob@castordoc.com"}
74
- table_with_owner = client._match_table_with_user(table, users_by_email)
93
+ table_with_owner = client._match_table_with_user(table, user_mapping)
75
94
 
76
95
  assert table_with_owner == {**table, "owner_external_id": 3}
77
96
 
78
97
  table_without_owner = {"id": 1, "owner_email": None}
79
- actual = client._match_table_with_user(table_without_owner, users_by_email)
98
+ actual = client._match_table_with_user(table_without_owner, user_mapping)
80
99
  assert actual == table_without_owner
@@ -127,13 +127,17 @@ class DatabricksFormatter:
127
127
  return email["value"]
128
128
  return emails[0]["value"]
129
129
 
130
+ def _email(self, user: dict) -> Optional[str]:
131
+ emails = user.get("emails")
132
+ return self._primary(emails) if emails else None
133
+
130
134
  def format_user(self, raw_users: List[dict]) -> List[dict]:
131
135
  users = []
132
136
  for user in raw_users:
133
137
  users.append(
134
138
  {
135
139
  "id": user["id"],
136
- "email": self._primary(user["emails"]),
140
+ "email": self._email(user),
137
141
  "first_name": None,
138
142
  "last_name": user.get("displayName") or user["userName"],
139
143
  "user_name": user["userName"],
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: castor-extractor
3
- Version: 0.16.6
3
+ Version: 0.16.9
4
4
  Summary: Extract your metadata assets.
5
5
  Home-page: https://www.castordoc.com/
6
6
  License: EULA
7
7
  Author: Castor
8
8
  Author-email: support@castordoc.com
9
- Requires-Python: >=3.8,<3.12
9
+ Requires-Python: >=3.8,<3.13
10
10
  Classifier: License :: Other/Proprietary License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.8
14
14
  Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
17
18
  Provides-Extra: all
18
19
  Provides-Extra: bigquery
19
20
  Provides-Extra: dbt
@@ -34,8 +35,10 @@ Requires-Dist: google-cloud-core (>=2.1.0,<3.0.0)
34
35
  Requires-Dist: google-cloud-storage (>=2,<3)
35
36
  Requires-Dist: google-resumable-media (>=2.0.3,<3.0.0)
36
37
  Requires-Dist: googleapis-common-protos (>=1.53.0,<2.0.0)
37
- Requires-Dist: looker-sdk (>=22.4.0,<=23.0.0) ; extra == "looker" or extra == "all"
38
+ Requires-Dist: looker-sdk (>=23.0.0) ; extra == "looker" or extra == "all"
38
39
  Requires-Dist: msal (>=1.20.0,<2.0.0) ; extra == "powerbi" or extra == "all"
40
+ Requires-Dist: numpy (<1.25) ; python_version >= "3.8" and python_version < "3.9"
41
+ Requires-Dist: numpy (>=1.26,<2) ; python_version >= "3.12" and python_version < "3.13"
39
42
  Requires-Dist: psycopg2-binary (>=2.0.0,<3.0.0) ; extra == "metabase" or extra == "postgres" or extra == "redshift" or extra == "all"
40
43
  Requires-Dist: pycryptodome (>=3.0.0,<4.0.0) ; extra == "metabase" or extra == "all"
41
44
  Requires-Dist: pydantic (>=2.6,<3.0)
@@ -1,4 +1,4 @@
1
- CHANGELOG.md,sha256=5zI5Mielu8ZXyckh7x2A_iXPW3qXjEhH_8THWOZVY0c,10191
1
+ CHANGELOG.md,sha256=WwEWPQQuGqVnWLhPtEh3SuOlBrNgHyHcLsYuvahpN7E,10437
2
2
  Dockerfile,sha256=HcX5z8OpeSvkScQsN-Y7CNMUig_UB6vTMDl7uqzuLGE,303
3
3
  LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
4
4
  README.md,sha256=uF6PXm9ocPITlKVSh9afTakHmpLx3TvawLf-CbMP3wM,3578
@@ -44,7 +44,7 @@ castor_extractor/uploader/env_test.py,sha256=ClCWWtwd2N-5ClIDUxVMeKkWfhhOTxpppsX
44
44
  castor_extractor/uploader/upload.py,sha256=5Aj3UOx8cpSVvzjYRz7S6nLk249IqUiCia70utU_970,3363
45
45
  castor_extractor/uploader/upload_test.py,sha256=BfGjAYEEDBmEcUS6_b3SlKyiQNR1iRf6-qmADDirTJI,328
46
46
  castor_extractor/uploader/utils.py,sha256=NCe0tkB28BVhqzOaDhDjaSfODjjcPWB17X6chnvyCWs,478
47
- castor_extractor/utils/__init__.py,sha256=cZbvEJ4G2IcJR2BzHwi3oOwDLqJsBx0J9gD71lWE1BQ,1149
47
+ castor_extractor/utils/__init__.py,sha256=bmzAOc-PKsVreMJtF7DGpPQeHrVqxWel_BblRftt6Ag,1186
48
48
  castor_extractor/utils/client/__init__.py,sha256=CRE-xJKm6fVV9dB8ljzB5YoOxX4I1sCD1KSgqs3Y8_Y,161
49
49
  castor_extractor/utils/client/abstract.py,sha256=aA5Qcb9TwWDSMq8WpXbGkOB20hehwX2VTpqQAwV76wk,2048
50
50
  castor_extractor/utils/client/api.py,sha256=tHa7eC11sS_eOCXhlnvUa2haRfOLENmjKgjB09Ijt0s,1664
@@ -53,7 +53,8 @@ castor_extractor/utils/client/postgres.py,sha256=n6ulaT222WWPY0_6qAZ0MHF0m91HtI9
53
53
  castor_extractor/utils/client/query.py,sha256=O6D5EjD1KmBlwa786Uw4D4kzxx97_HH50xIIeSWt0B8,205
54
54
  castor_extractor/utils/client/uri.py,sha256=jmP9hY-6PRqdc3-vAOdtll_U6q9VCqSqmBAN6QRs3ZI,150
55
55
  castor_extractor/utils/client/uri_test.py,sha256=1XKF6qSseCeD4G4ckaNO07JXfGbt7XUVinOZdpEYrDQ,259
56
- castor_extractor/utils/collection.py,sha256=uenJvfamphxV5ZFt12BgfsRs99pWffYJIMjAD_Laz2Q,417
56
+ castor_extractor/utils/collection.py,sha256=L4KLCjpgYBEwC7lSPFWBkiUzeLOTv5e-X6c-nibdnmQ,1555
57
+ castor_extractor/utils/collection_test.py,sha256=I9xLnBUPiAGj7UDqEd0G6pcgqjcNHJ5DYXbmTXVhBM8,1713
57
58
  castor_extractor/utils/constants.py,sha256=qBQprS9U66mS-RIBXiLujdTSV3WvGv40Bc0khP4Abdk,39
58
59
  castor_extractor/utils/dbt/__init__.py,sha256=LHQROlMqYWCc7tcmhdjXtROFpJqUvCg9jPC8avHgD4I,107
59
60
  castor_extractor/utils/dbt/assets.py,sha256=JY1nKEGySZ84wNoe7dnizwAYw2q0t8NVaIfqhB2rSw0,148
@@ -92,8 +93,8 @@ castor_extractor/utils/salesforce/credentials_test.py,sha256=FQRyNk2Jsh6KtYiW20o
92
93
  castor_extractor/utils/store.py,sha256=D_pVaPsu1MKAJC0K47O_vYTs-Afl6oejravAJdvjmGc,2040
93
94
  castor_extractor/utils/string.py,sha256=aW6bbjqEGnh9kT5KZBnMlV6fhdgOJ0ENCkCTDon1xA0,2377
94
95
  castor_extractor/utils/string_test.py,sha256=OmRVCJUXMcCTwY-QJDhUViYpxkvQQgNRJLCaXY0iUnk,2535
95
- castor_extractor/utils/time.py,sha256=a9AChz_rlmPwLxQvoC0tF_fNUJvBVCTLQSdJLtN107w,1210
96
- castor_extractor/utils/time_test.py,sha256=W51zOsYNu_BNzsl3EWeT0Pgn2lUbT_weT8bM5rTrIQk,283
96
+ castor_extractor/utils/time.py,sha256=Rll1KJVDZdCKjWuiXA_BuB9OHeEfb_f6Z637Kkxnobs,1385
97
+ castor_extractor/utils/time_test.py,sha256=pEwpcHI7wGPnfgwrH1DNHEbPz3HEAryNF5yPL7Dqkp8,448
97
98
  castor_extractor/utils/type.py,sha256=87t32cTctEjX-_BqZLtPLWu-M9OVvw_lFU4DbaQ6V0U,313
98
99
  castor_extractor/utils/validation.py,sha256=NNMkdyvMzConslnyCM3gmciEtPPvefW0vAT2gNsMhvE,1909
99
100
  castor_extractor/utils/validation_test.py,sha256=aSetitOCkH_K-Wto9ISOVGso5jGfTUOBLm3AZnvavO8,1181
@@ -102,29 +103,29 @@ castor_extractor/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
102
103
  castor_extractor/visualization/domo/__init__.py,sha256=_mAYVfoVLizfLGF_f6ZiwBhdPpvoJY_diySf33dt3Jo,127
103
104
  castor_extractor/visualization/domo/assets.py,sha256=bK1urFR2tnlWkVkkhR32mAKMoKbESNlop-CNGx-65PY,206
104
105
  castor_extractor/visualization/domo/client/__init__.py,sha256=UDszV3IXNC9Wp_j55NZ-6ey2INo0TYtAg2QNIJOjglE,88
105
- castor_extractor/visualization/domo/client/client.py,sha256=ZJJh0ZWuh1X4_uW6FEcZUUAxgRRVFh9vWUtS5aJly4Q,11075
106
- castor_extractor/visualization/domo/client/client_test.py,sha256=qGGmpJHnm-o2Ybko5J31nSM9xev5YS0yXjVNM9E92b4,2378
106
+ castor_extractor/visualization/domo/client/client.py,sha256=8bQgK6TFJcEvSH19ZzVNlsyz86jgIypOAheOCXLORVU,10203
107
+ castor_extractor/visualization/domo/client/client_test.py,sha256=pEIL8AALixq9a7sckW-TVqaz1k4oKUtfGUjGpTPOJi0,1790
107
108
  castor_extractor/visualization/domo/client/credentials.py,sha256=CksQ9W9X6IGjTlYN0okwGAmURMRJKAjctxODAvAJUAo,1148
108
- castor_extractor/visualization/domo/client/endpoints.py,sha256=_D_gq99d77am4KHeRCkITM_XyYuAOscHagC7t3adscY,2019
109
- castor_extractor/visualization/domo/client/pagination.py,sha256=E3WMK9Uw-u5qt9LCUzwKdKh9oSzyFEC0GgnRMFgxgrs,713
109
+ castor_extractor/visualization/domo/client/endpoints.py,sha256=6UI5psMYaIa1Pq_Gulb4cNna7NZ16xMotScX7yg5TRQ,2367
110
+ castor_extractor/visualization/domo/client/pagination.py,sha256=ukVkHVzoH4mfZ29H9YcnC2YrdVolP10wv25J6Q3ehRw,821
110
111
  castor_extractor/visualization/domo/client/pagination_test.py,sha256=nV4yZWfus13QFCr-tlBUgwva21VqfpF6P-0ks_Awwis,581
111
112
  castor_extractor/visualization/domo/constants.py,sha256=AriJZPrCY5Z3HRUANrMu-4U0b7hQK_jRDcxiB-hbrQ4,233
112
113
  castor_extractor/visualization/domo/extract.py,sha256=GWWRfPEMt4SgzBGFaTcoOabsoOqLRFIEFAtgXwb8LDI,2567
113
114
  castor_extractor/visualization/looker/__init__.py,sha256=Xu5bJ3743kaP8szMMp2NXCgvM1EdOQgtic4utUlO9Cc,145
114
115
  castor_extractor/visualization/looker/api/__init__.py,sha256=rN03VMucxIqc0yfd17dIe3ZNFpcg5CA09epn1fKJg90,99
115
- castor_extractor/visualization/looker/api/client.py,sha256=R-hnVK1TYaCPYaJ1wvpVUwa_AqQPu2RAcZz0kOK4l58,9582
116
+ castor_extractor/visualization/looker/api/client.py,sha256=tfV-zx5FAtPCtu_NFIxfSFiQ1AtxxIeAj1sHT5Vug3I,9790
116
117
  castor_extractor/visualization/looker/api/client_test.py,sha256=wsi20-neBXHaahDqf4nwCp8Ew5fRFCmVHG3OqrePKFs,1868
117
118
  castor_extractor/visualization/looker/api/constants.py,sha256=pZpq09tqcGi2Vh8orXxn9eil8ewfPUOLKfVuqgV2W-A,4126
118
- castor_extractor/visualization/looker/api/sdk.py,sha256=hSNcRsCoFae3zmjWFGsMrhQCIP57TcMJ2SorMPYJwn4,3553
119
+ castor_extractor/visualization/looker/api/sdk.py,sha256=RiCb-3BAL59iGqLfPEO9490cM05LRv-xGoo-AM-Z4No,2251
119
120
  castor_extractor/visualization/looker/api/sdk_test.py,sha256=NHtKZTflPhqzBFHs1TyAQaubgxfzLLwYKFT8rEqR55I,1742
120
- castor_extractor/visualization/looker/api/utils.py,sha256=NpP90CA-SwdUjHhaWFBsKpJz0Z9BXgDOahIqfc3R9tk,565
121
+ castor_extractor/visualization/looker/api/utils.py,sha256=TJqq9UBVFtS33VB1zHzT6kU8f9MOyqXsUcr57ZurJy4,588
121
122
  castor_extractor/visualization/looker/assets.py,sha256=K08nV6MMIpfF9r91TmCO7_62smHzGRv3gR4aIOootMQ,827
122
123
  castor_extractor/visualization/looker/constant.py,sha256=JvmAfbyAg-LQIoI48Slg9k5T0mYggs_76D_6vD-mP88,695
123
124
  castor_extractor/visualization/looker/env.py,sha256=vPqirdeGKm3as2T-tBTjbpulQe8W7-3UE2j-Z57wFXk,1174
124
- castor_extractor/visualization/looker/extract.py,sha256=vOIP8Hoxv05MiRa-l79YKOCHahuNiSW9uSKjwQQQQKs,5112
125
+ castor_extractor/visualization/looker/extract.py,sha256=Y7ZsiUO3uaAkBX0inhjMcEXX4nyBTo9ewthtLfbQhzU,5132
125
126
  castor_extractor/visualization/looker/fields.py,sha256=WmiSehmczWTufCLg4r2Ozq2grUpzxDNvIAHyGuOoGs4,636
126
127
  castor_extractor/visualization/looker/fields_test.py,sha256=7Cwq8Qky6aTZg8nCHp1gmPJtd9pGNB4QeMIRRWdHo5w,782
127
- castor_extractor/visualization/looker/multithreading.py,sha256=WtsJlwSarE27qSLO6nc2Tudv15lZqXV5B4NmO0nqxFE,2602
128
+ castor_extractor/visualization/looker/multithreading.py,sha256=6CrMOy9kMBfhHnZI7XrpNNyBiYYCO3CE4AuIjQVlLH0,2610
128
129
  castor_extractor/visualization/looker/parameters.py,sha256=Nk2hfrg3L9twU-51Q7Wdp9uaxy8M2_juEebWoLfIMPc,2427
129
130
  castor_extractor/visualization/metabase/__init__.py,sha256=hSIoVgPzhQh-9H8XRUzga4EZSOYejGdH-qY_hBNGbyw,125
130
131
  castor_extractor/visualization/metabase/assets.py,sha256=47fMax7QjFKyjEuH2twsjd-l9EM4RJutAZb4GbASdkU,2836
@@ -243,11 +244,11 @@ castor_extractor/visualization/tableau/usage.py,sha256=LlFwlbEr-EnYUJjKZha99CRCR
243
244
  castor_extractor/visualization/tableau_revamp/__init__.py,sha256=a3DGjQhaz17gBqW-E84TAgupKbqLC40y5Ajo1yn-ot4,156
244
245
  castor_extractor/visualization/tableau_revamp/assets.py,sha256=owlwaI2E4UKk1YhkaHgaAXx6gu3Op6EqZ7bjp0tHI6s,351
245
246
  castor_extractor/visualization/tableau_revamp/client/__init__.py,sha256=wmS9uLtUiqNYVloi0-DgD8d2qzu3RVZEAtWiaDp6G_M,90
246
- castor_extractor/visualization/tableau_revamp/client/client.py,sha256=PsU7OOAYeTBF4dcT-Tl7RwJGD8o_S6vmGhUk_lYNhDw,7288
247
+ castor_extractor/visualization/tableau_revamp/client/client.py,sha256=8BO7J-HFM2j6_f-Hjj3uSWip11eKeZ0cjhxGEqMTPRA,9428
247
248
  castor_extractor/visualization/tableau_revamp/client/credentials.py,sha256=fHG32egq6ll2U4BNazalMof_plzfCMQjrN9WOs6kezk,3014
248
249
  castor_extractor/visualization/tableau_revamp/client/errors.py,sha256=dTe1shqmWmAXpDpCz-E24m8dGYjt6rvIGV9qQb4jnvI,150
249
- castor_extractor/visualization/tableau_revamp/client/gql_queries.py,sha256=SiKTbl-lblV6GCajikPXrDh4BaTmi0wN_HtGQhVDV3o,2041
250
- castor_extractor/visualization/tableau_revamp/client/tsc_fields.py,sha256=Nl_CM2OEzgh3eL8Szcv9Fbiu9wGRkcaYlerUYTJrZLQ,610
250
+ castor_extractor/visualization/tableau_revamp/client/gql_queries.py,sha256=jBxvjQnOIWfFjMJpr7S_ZPnQhdzabxoO3jyEKi8A8ns,2112
251
+ castor_extractor/visualization/tableau_revamp/client/tsc_fields.py,sha256=WsDliPCo-XsQ7wN-j0gpW9bdxCHvgH-aePywiltzfbU,688
251
252
  castor_extractor/visualization/tableau_revamp/constants.py,sha256=PcdudAogQhi3e-knalhgliMKjy5ahN0em_-7XSLrnxM,87
252
253
  castor_extractor/visualization/tableau_revamp/extract.py,sha256=2SLUxp5okM4AcEJJ61ZgcC2ikfZZl9MH17CEXMXmgl0,1450
253
254
  castor_extractor/warehouse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -276,11 +277,11 @@ castor_extractor/warehouse/bigquery/queries/view_ddl.sql,sha256=obCm-IN9V8_YSZTw
276
277
  castor_extractor/warehouse/bigquery/query.py,sha256=hrFfjd5jW2oQnZ6ozlkn-gDe6sCIzu5zSX19T9W6fIk,4162
277
278
  castor_extractor/warehouse/bigquery/types.py,sha256=LZVWSmE57lOemNbB5hBRyYmDk9bFAU4nbRaJWALl6N8,140
278
279
  castor_extractor/warehouse/databricks/__init__.py,sha256=bTvDxjGQGM2J3hOnVhfNmFP1y8DK0tySiD_EXe5_xWE,200
279
- castor_extractor/warehouse/databricks/client.py,sha256=FIqHjlGN5EN2dvcZD2941zPAomOye91JmkgPlxGDk0g,8078
280
- castor_extractor/warehouse/databricks/client_test.py,sha256=ctOQnUXosuuFjWGJKgkxjUcV4vQUBWt2BQ_f0Tyzqe4,2717
280
+ castor_extractor/warehouse/databricks/client.py,sha256=FsHlpHZ9JTG92Rf_8Z7277o9HBaAD0CKxSEHiujOgXg,8271
281
+ castor_extractor/warehouse/databricks/client_test.py,sha256=Y-LBveZFRVaaL49Lo2MwbcJReBcYLNRdHtR_w7xWNWQ,3381
281
282
  castor_extractor/warehouse/databricks/credentials.py,sha256=PpGv5_GP320UQjV_gvaxSpOw58AmqSznmjGhGfe6bdU,655
282
283
  castor_extractor/warehouse/databricks/extract.py,sha256=-vJhAIxSu1lD_xGl-GXZYTmc5BGu0aXM3l-U0UghREM,5773
283
- castor_extractor/warehouse/databricks/format.py,sha256=LiPGCTPzL3gQQMMl1v6DvpcTk7BWxZFq03jnHdoYnuU,4968
284
+ castor_extractor/warehouse/databricks/format.py,sha256=Nd5L89yWhpIl0OEMV7WK1H3JYUa9WGPC0c-NUOT_uXM,5101
284
285
  castor_extractor/warehouse/databricks/format_test.py,sha256=iPmdJof43fBYL1Sa_fBrCWDQHCHgm7IWCZag1kWkj9E,1970
285
286
  castor_extractor/warehouse/databricks/types.py,sha256=T2SyLy9pY_olLtstdC77moPxIiikVsuQLMxh92YMJQo,78
286
287
  castor_extractor/warehouse/mysql/__init__.py,sha256=2KFDogo9GNbApHqw3Vm5t_uNmIRjdp76nmP_WQQMfQY,116
@@ -367,8 +368,8 @@ castor_extractor/warehouse/synapse/queries/schema.sql,sha256=aX9xNrBD_ydwl-znGSF
367
368
  castor_extractor/warehouse/synapse/queries/table.sql,sha256=mCE8bR1Vb7j7SwZW2gafcXidQ2fo1HwxcybA8wP2Kfs,1049
368
369
  castor_extractor/warehouse/synapse/queries/user.sql,sha256=sTb_SS7Zj3AXW1SggKPLNMCd0qoTpL7XI_BJRMaEpBg,67
369
370
  castor_extractor/warehouse/synapse/queries/view_ddl.sql,sha256=3EVbp5_yTgdByHFIPLHmnoOnqqLE77SrjAwFDvu4e54,249
370
- castor_extractor-0.16.6.dist-info/LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
371
- castor_extractor-0.16.6.dist-info/METADATA,sha256=yDrJkKrR_JGhxTHdorJu_IcuH_d3qd7VLoLwQPEdclo,6370
372
- castor_extractor-0.16.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
373
- castor_extractor-0.16.6.dist-info/entry_points.txt,sha256=SbyPk58Gh-FRztfCNnUZQ6w7SatzNJFZ6GIJLNsy7tI,1427
374
- castor_extractor-0.16.6.dist-info/RECORD,,
371
+ castor_extractor-0.16.9.dist-info/LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
372
+ castor_extractor-0.16.9.dist-info/METADATA,sha256=qRP78w8BztI4N8IyOLoESkFdhKWByXf7PQQjFLTvu6A,6582
373
+ castor_extractor-0.16.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
374
+ castor_extractor-0.16.9.dist-info/entry_points.txt,sha256=SbyPk58Gh-FRztfCNnUZQ6w7SatzNJFZ6GIJLNsy7tI,1427
375
+ castor_extractor-0.16.9.dist-info/RECORD,,