castor-extractor 0.24.55__py3-none-any.whl → 0.25.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of castor-extractor might be problematic. Click here for more details.
- CHANGELOG.md +23 -0
- castor_extractor/commands/extract_count.py +22 -0
- castor_extractor/commands/extract_powerbi.py +12 -1
- castor_extractor/visualization/count/__init__.py +3 -0
- castor_extractor/visualization/count/assets.py +11 -0
- castor_extractor/visualization/count/client/__init__.py +2 -0
- castor_extractor/visualization/count/client/client.py +50 -0
- castor_extractor/visualization/count/client/credentials.py +10 -0
- castor_extractor/visualization/count/client/queries/canvas_permissions.sql +6 -0
- castor_extractor/visualization/count/client/queries/canvases.sql +6 -0
- castor_extractor/visualization/count/client/queries/cells.sql +8 -0
- castor_extractor/visualization/count/client/queries/projects.sql +5 -0
- castor_extractor/visualization/count/client/queries/users.sql +8 -0
- castor_extractor/visualization/count/extract.py +54 -0
- castor_extractor/visualization/powerbi/client/__init__.py +1 -0
- castor_extractor/visualization/powerbi/client/authentication.py +20 -2
- castor_extractor/visualization/powerbi/client/credentials.py +9 -2
- castor_extractor/visualization/powerbi/extract.py +23 -3
- castor_extractor/visualization/sigma/assets.py +1 -1
- castor_extractor/visualization/sigma/client/client.py +45 -108
- castor_extractor/visualization/sigma/client/endpoints.py +4 -0
- castor_extractor/visualization/sigma/client/pagination.py +21 -1
- castor_extractor/visualization/sigma/client/sources_transformer.py +28 -9
- castor_extractor/visualization/sigma/extract.py +8 -6
- {castor_extractor-0.24.55.dist-info → castor_extractor-0.25.2.dist-info}/METADATA +26 -2
- {castor_extractor-0.24.55.dist-info → castor_extractor-0.25.2.dist-info}/RECORD +29 -17
- {castor_extractor-0.24.55.dist-info → castor_extractor-0.25.2.dist-info}/entry_points.txt +1 -0
- {castor_extractor-0.24.55.dist-info → castor_extractor-0.25.2.dist-info}/LICENCE +0 -0
- {castor_extractor-0.24.55.dist-info → castor_extractor-0.25.2.dist-info}/WHEEL +0 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.2 - 2025-09-30
|
|
4
|
+
|
|
5
|
+
* PowerBi: Support auth with private_key
|
|
6
|
+
|
|
7
|
+
## 0.25.1 - 2025-09-29
|
|
8
|
+
|
|
9
|
+
* Sigma: catch ReadTimeouts during elements extraction
|
|
10
|
+
|
|
11
|
+
## 0.25.0 - 2025-09-15
|
|
12
|
+
|
|
13
|
+
* Count: adding connector
|
|
14
|
+
|
|
15
|
+
## 0.24.57 - 2025-09-24
|
|
16
|
+
|
|
17
|
+
* Sigma:
|
|
18
|
+
* fix pagination
|
|
19
|
+
* remove redundant element lineages endpoint
|
|
20
|
+
* extract data model sources
|
|
21
|
+
|
|
22
|
+
## 0.24.56 - 2025-09-24
|
|
23
|
+
|
|
24
|
+
* bump dependencies
|
|
25
|
+
|
|
3
26
|
## 0.24.55 - 2025-09-19
|
|
4
27
|
|
|
5
28
|
* Fix encoding in LocalStorage - force to utf-8
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
|
|
3
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
|
+
from castor_extractor.visualization import count # type: ignore
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = ArgumentParser()
|
|
9
|
+
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"-c",
|
|
12
|
+
"--credentials",
|
|
13
|
+
help="GCP credentials as string",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"-d",
|
|
18
|
+
"--dataset_id",
|
|
19
|
+
help="dataset id, where count info is stored for the current customer",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
count.extract_all(**parse_filled_arguments(parser))
|
|
@@ -9,10 +9,21 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
|
9
9
|
|
|
10
10
|
def main():
|
|
11
11
|
parser = ArgumentParser()
|
|
12
|
+
auth_group = parser.add_mutually_exclusive_group(required=True)
|
|
12
13
|
|
|
13
14
|
parser.add_argument("-t", "--tenant_id", help="PowerBi tenant ID")
|
|
14
15
|
parser.add_argument("-c", "--client_id", help="PowerBi client ID")
|
|
15
|
-
|
|
16
|
+
auth_group.add_argument(
|
|
17
|
+
"-s",
|
|
18
|
+
"--secret",
|
|
19
|
+
help="PowerBi password as a string",
|
|
20
|
+
)
|
|
21
|
+
auth_group.add_argument(
|
|
22
|
+
"-cert",
|
|
23
|
+
"--certificate",
|
|
24
|
+
help="file path to json certificate file with"
|
|
25
|
+
"keys: private_key, thumbprint, public_certificate",
|
|
26
|
+
)
|
|
16
27
|
parser.add_argument(
|
|
17
28
|
"-sc",
|
|
18
29
|
"--scopes",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from typing import Any, Iterator
|
|
4
|
+
|
|
5
|
+
from ....utils import load_file
|
|
6
|
+
from ....warehouse.bigquery import BigQueryClient
|
|
7
|
+
from ..assets import (
|
|
8
|
+
CountAsset,
|
|
9
|
+
)
|
|
10
|
+
from .credentials import CountCredentials
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_QUERIES_FOLDER = "queries"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CountClient(BigQueryClient):
|
|
18
|
+
"""
|
|
19
|
+
Count.co does not currently provide an official API.
|
|
20
|
+
Instead, metadata such as dashboards, users, and queries is made available through
|
|
21
|
+
special metadata tables stored in BigQuery.
|
|
22
|
+
|
|
23
|
+
This client extends `BigQueryClient` to access and interact with those metadata tables.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, credentials: CountCredentials):
|
|
27
|
+
super().__init__(asdict(credentials))
|
|
28
|
+
self.project_id = credentials.project_id
|
|
29
|
+
self.dataset_id = credentials.dataset_id
|
|
30
|
+
|
|
31
|
+
def _load_query(self, asset: CountAsset) -> str:
|
|
32
|
+
query = load_file(
|
|
33
|
+
f"{_QUERIES_FOLDER}/{asset.name.lower()}.sql", __file__
|
|
34
|
+
)
|
|
35
|
+
return query.format(
|
|
36
|
+
project_id=self.project_id, dataset_id=self.dataset_id
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def fetch(self, asset: CountAsset) -> Iterator[dict[str, Any]]:
|
|
40
|
+
"""
|
|
41
|
+
Fetch the asset given as param, by running a BigQuery query.
|
|
42
|
+
"""
|
|
43
|
+
logger.info(f"Running BigQuery query to fetch: {asset.name}")
|
|
44
|
+
|
|
45
|
+
query_str = self._load_query(asset)
|
|
46
|
+
job = self.client.query(query_str)
|
|
47
|
+
results = job.result()
|
|
48
|
+
|
|
49
|
+
for row in results:
|
|
50
|
+
yield dict(row)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from pydantic.dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ....warehouse.bigquery import BigQueryCredentials
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CountCredentials(BigQueryCredentials):
|
|
8
|
+
"""Count credentials extending BigQuery credentials with additional dataset information"""
|
|
9
|
+
|
|
10
|
+
dataset_id: str
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Iterable, Iterator, Union
|
|
3
|
+
|
|
4
|
+
from ...utils import (
|
|
5
|
+
OUTPUT_DIR,
|
|
6
|
+
current_timestamp,
|
|
7
|
+
deep_serialize,
|
|
8
|
+
from_env,
|
|
9
|
+
get_output_filename,
|
|
10
|
+
write_json,
|
|
11
|
+
write_summary,
|
|
12
|
+
)
|
|
13
|
+
from .assets import (
|
|
14
|
+
CountAsset,
|
|
15
|
+
)
|
|
16
|
+
from .client import (
|
|
17
|
+
CountClient,
|
|
18
|
+
CountCredentials,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def iterate_all_data(
|
|
25
|
+
client: CountClient,
|
|
26
|
+
) -> Iterable[tuple[CountAsset, Union[list, Iterator, dict]]]:
|
|
27
|
+
"""Iterate over the extracted data from count"""
|
|
28
|
+
|
|
29
|
+
for asset in CountAsset:
|
|
30
|
+
logger.info(f"Extracting {asset.value} from API")
|
|
31
|
+
data = client.fetch(asset)
|
|
32
|
+
yield asset, deep_serialize(data)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extract_all(**kwargs) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Extract data from count BigQuery project
|
|
38
|
+
Store the output files locally under the given output_directory
|
|
39
|
+
"""
|
|
40
|
+
_output_directory = kwargs.get("output") or from_env(OUTPUT_DIR)
|
|
41
|
+
dataset_id = kwargs.get("dataset_id")
|
|
42
|
+
if not dataset_id:
|
|
43
|
+
raise ValueError("dataset_id is required")
|
|
44
|
+
|
|
45
|
+
credentials = CountCredentials(**kwargs)
|
|
46
|
+
client = CountClient(credentials=credentials)
|
|
47
|
+
|
|
48
|
+
ts = current_timestamp()
|
|
49
|
+
|
|
50
|
+
for key, data in iterate_all_data(client):
|
|
51
|
+
filename = get_output_filename(key.name.lower(), _output_directory, ts)
|
|
52
|
+
write_json(filename, list(data))
|
|
53
|
+
|
|
54
|
+
write_summary(_output_directory, ts)
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
1
3
|
import msal # type: ignore
|
|
2
4
|
|
|
3
5
|
from ....utils import BearerAuth
|
|
4
6
|
from .constants import Keys
|
|
5
|
-
from .credentials import PowerbiCredentials
|
|
7
|
+
from .credentials import PowerbiCertificate, PowerbiCredentials
|
|
6
8
|
from .endpoints import PowerBiEndpointFactory
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
def _get_client_credential(
|
|
12
|
+
secret: Optional[str], certificate: Optional[PowerbiCertificate]
|
|
13
|
+
) -> Union[str, dict]:
|
|
14
|
+
if secret:
|
|
15
|
+
return secret
|
|
16
|
+
if certificate:
|
|
17
|
+
return certificate.model_dump()
|
|
18
|
+
|
|
19
|
+
raise ValueError("Either certificate or secret must be provided.")
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
class PowerBiBearerAuth(BearerAuth):
|
|
10
23
|
def __init__(self, credentials: PowerbiCredentials):
|
|
11
24
|
self.credentials = credentials
|
|
@@ -14,10 +27,15 @@ class PowerBiBearerAuth(BearerAuth):
|
|
|
14
27
|
api_base=self.credentials.api_base,
|
|
15
28
|
)
|
|
16
29
|
authority = endpoint_factory.authority(self.credentials.tenant_id)
|
|
30
|
+
|
|
31
|
+
client_credential = _get_client_credential(
|
|
32
|
+
self.credentials.secret, self.credentials.certificate
|
|
33
|
+
)
|
|
34
|
+
|
|
17
35
|
self.app = msal.ConfidentialClientApplication(
|
|
18
36
|
client_id=self.credentials.client_id,
|
|
19
37
|
authority=authority,
|
|
20
|
-
client_credential=
|
|
38
|
+
client_credential=client_credential,
|
|
21
39
|
)
|
|
22
40
|
|
|
23
41
|
def fetch_token(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from pydantic import
|
|
3
|
+
from pydantic import BaseModel, field_validator
|
|
4
4
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
5
|
|
|
6
6
|
DEFAULT_SCOPE = "https://analysis.windows.net/powerbi/api/.default"
|
|
@@ -10,6 +10,12 @@ CLIENT_APP_BASE = "https://login.microsoftonline.com"
|
|
|
10
10
|
REST_API_BASE_PATH = "https://api.powerbi.com/v1.0/myorg"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class PowerbiCertificate(BaseModel):
|
|
14
|
+
public_certificate: Optional[str] = None
|
|
15
|
+
private_key: str
|
|
16
|
+
thumbprint: str
|
|
17
|
+
|
|
18
|
+
|
|
13
19
|
class PowerbiCredentials(BaseSettings):
|
|
14
20
|
"""Class to handle PowerBI rest API permissions"""
|
|
15
21
|
|
|
@@ -21,7 +27,8 @@ class PowerbiCredentials(BaseSettings):
|
|
|
21
27
|
|
|
22
28
|
client_id: str
|
|
23
29
|
tenant_id: str
|
|
24
|
-
secret: str =
|
|
30
|
+
secret: Optional[str] = None
|
|
31
|
+
certificate: Optional[PowerbiCertificate] = None
|
|
25
32
|
api_base: str = REST_API_BASE_PATH
|
|
26
33
|
login_url: str = CLIENT_APP_BASE
|
|
27
34
|
scopes: list[str] = [DEFAULT_SCOPE]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
from collections.abc import Iterable
|
|
3
|
-
from typing import Union
|
|
4
|
+
from typing import Optional, Union
|
|
4
5
|
|
|
5
6
|
from ...utils import (
|
|
6
7
|
OUTPUT_DIR,
|
|
@@ -12,11 +13,22 @@ from ...utils import (
|
|
|
12
13
|
write_summary,
|
|
13
14
|
)
|
|
14
15
|
from .assets import PowerBiAsset
|
|
15
|
-
from .client import PowerbiClient, PowerbiCredentials
|
|
16
|
+
from .client import PowerbiCertificate, PowerbiClient, PowerbiCredentials
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def _load_certificate(
|
|
22
|
+
certificate: Optional[str],
|
|
23
|
+
) -> Optional[PowerbiCertificate]:
|
|
24
|
+
if not certificate:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
with open(certificate) as file:
|
|
28
|
+
cert = json.load(file)
|
|
29
|
+
return PowerbiCertificate(**cert)
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
def iterate_all_data(
|
|
21
33
|
client: PowerbiClient,
|
|
22
34
|
) -> Iterable[tuple[PowerBiAsset, Union[list, dict]]]:
|
|
@@ -36,7 +48,15 @@ def extract_all(**kwargs) -> None:
|
|
|
36
48
|
Store the output files locally under the given output_directory
|
|
37
49
|
"""
|
|
38
50
|
_output_directory = kwargs.get("output") or from_env(OUTPUT_DIR)
|
|
39
|
-
creds = PowerbiCredentials(
|
|
51
|
+
creds = PowerbiCredentials(
|
|
52
|
+
client_id=kwargs.get("client_id"),
|
|
53
|
+
tenant_id=kwargs.get("tenant_id"),
|
|
54
|
+
secret=kwargs.get("secret"),
|
|
55
|
+
certificate=_load_certificate(kwargs.get("certificate")),
|
|
56
|
+
api_base=kwargs.get("api_base"),
|
|
57
|
+
login_url=kwargs.get("login_url"),
|
|
58
|
+
scopes=kwargs.get("scopes"),
|
|
59
|
+
)
|
|
40
60
|
client = PowerbiClient(creds)
|
|
41
61
|
ts = current_timestamp()
|
|
42
62
|
|
|
@@ -5,11 +5,11 @@ class SigmaAsset(ExternalAsset):
|
|
|
5
5
|
"""Sigma assets"""
|
|
6
6
|
|
|
7
7
|
DATAMODELS = "datamodels"
|
|
8
|
+
DATAMODEL_SOURCES = "datamodel_sources"
|
|
8
9
|
DATASETS = "datasets"
|
|
9
10
|
DATASET_SOURCES = "dataset_sources"
|
|
10
11
|
ELEMENTS = "elements"
|
|
11
12
|
FILES = "files"
|
|
12
|
-
LINEAGES = "lineages"
|
|
13
13
|
MEMBERS = "members"
|
|
14
14
|
QUERIES = "queries"
|
|
15
15
|
WORKBOOKS = "workbooks"
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections.abc import Iterator
|
|
3
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
4
3
|
from functools import partial
|
|
5
4
|
from http import HTTPStatus
|
|
6
5
|
from typing import Callable, Iterable, Optional
|
|
7
6
|
|
|
8
|
-
from
|
|
7
|
+
from requests import ReadTimeout
|
|
9
8
|
|
|
10
9
|
from ....utils import (
|
|
11
10
|
APIClient,
|
|
12
11
|
RequestSafeMode,
|
|
13
12
|
fetch_all_pages,
|
|
14
|
-
retry,
|
|
15
13
|
)
|
|
16
14
|
from ..assets import SigmaAsset
|
|
17
15
|
from .authentication import SigmaBearerAuth
|
|
@@ -55,38 +53,12 @@ SIGMA_SAFE_MODE = RequestSafeMode(
|
|
|
55
53
|
max_errors=_VOLUME_IGNORED,
|
|
56
54
|
status_codes=_IGNORED_ERROR_CODES,
|
|
57
55
|
)
|
|
58
|
-
SIGMA_SAFE_MODE_LINEAGE = RequestSafeMode(
|
|
59
|
-
max_errors=_VOLUME_IGNORED,
|
|
60
|
-
status_codes=(
|
|
61
|
-
*_IGNORED_ERROR_CODES,
|
|
62
|
-
HTTPStatus.FORBIDDEN,
|
|
63
|
-
),
|
|
64
|
-
)
|
|
65
|
-
_THREADS_LINEAGE = 10 # empirically found; hit the rate limit with 20 workers
|
|
66
56
|
_RETRY_NUMBER = 1
|
|
67
57
|
_RETRY_BASE_MS = 60_000
|
|
68
58
|
|
|
69
59
|
|
|
70
|
-
class LineageContext(BaseModel):
|
|
71
|
-
"""all info needed to build the endpoint for lineage retrieval"""
|
|
72
|
-
|
|
73
|
-
workbook_id: str
|
|
74
|
-
element_id: str
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class Lineage(BaseModel):
|
|
78
|
-
"""holds response from lineage API and context used to retrieve it"""
|
|
79
|
-
|
|
80
|
-
lineage: dict
|
|
81
|
-
context: LineageContext
|
|
82
|
-
|
|
83
|
-
|
|
84
60
|
class SigmaClient(APIClient):
|
|
85
|
-
def __init__(
|
|
86
|
-
self,
|
|
87
|
-
credentials: SigmaCredentials,
|
|
88
|
-
safe_mode: Optional[RequestSafeMode] = None,
|
|
89
|
-
):
|
|
61
|
+
def __init__(self, credentials: SigmaCredentials):
|
|
90
62
|
auth = SigmaBearerAuth(
|
|
91
63
|
host=credentials.host,
|
|
92
64
|
token_payload=credentials.token_payload,
|
|
@@ -96,7 +68,7 @@ class SigmaClient(APIClient):
|
|
|
96
68
|
auth=auth,
|
|
97
69
|
headers=_SIGMA_HEADERS,
|
|
98
70
|
timeout=_SIGMA_TIMEOUT_S,
|
|
99
|
-
safe_mode=
|
|
71
|
+
safe_mode=SIGMA_SAFE_MODE,
|
|
100
72
|
)
|
|
101
73
|
|
|
102
74
|
def _get_paginated(
|
|
@@ -144,6 +116,31 @@ class SigmaClient(APIClient):
|
|
|
144
116
|
request = self._get_paginated(endpoint=SigmaEndpointFactory.workbooks())
|
|
145
117
|
yield from fetch_all_pages(request, SigmaPagination)
|
|
146
118
|
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _safe_fetch_elements(
|
|
121
|
+
elements: Iterator[dict],
|
|
122
|
+
workbook_id: str,
|
|
123
|
+
page_id: str,
|
|
124
|
+
) -> Iterator[dict]:
|
|
125
|
+
"""
|
|
126
|
+
Safely iterates over elements with ReadTimeout handling. In case of
|
|
127
|
+
said error, it skips the entire rest of the page.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
for element in elements:
|
|
131
|
+
if element.get("type") not in _DATA_ELEMENTS:
|
|
132
|
+
continue
|
|
133
|
+
yield {
|
|
134
|
+
**element,
|
|
135
|
+
"workbook_id": workbook_id,
|
|
136
|
+
"page_id": page_id,
|
|
137
|
+
}
|
|
138
|
+
except ReadTimeout:
|
|
139
|
+
logger.warning(
|
|
140
|
+
f"ReadTimeout for page {page_id} in workbook {workbook_id}"
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
147
144
|
def _get_elements_per_page(
|
|
148
145
|
self, page: dict, workbook_id: str
|
|
149
146
|
) -> Iterator[dict]:
|
|
@@ -152,14 +149,7 @@ class SigmaClient(APIClient):
|
|
|
152
149
|
SigmaEndpointFactory.elements(workbook_id, page_id)
|
|
153
150
|
)
|
|
154
151
|
elements = fetch_all_pages(request, SigmaPagination)
|
|
155
|
-
|
|
156
|
-
if element.get("type") not in _DATA_ELEMENTS:
|
|
157
|
-
continue
|
|
158
|
-
yield {
|
|
159
|
-
**element,
|
|
160
|
-
"workbook_id": workbook_id,
|
|
161
|
-
"page_id": page_id,
|
|
162
|
-
}
|
|
152
|
+
yield from self._safe_fetch_elements(elements, workbook_id, page_id)
|
|
163
153
|
|
|
164
154
|
def _get_all_elements(self, workbooks: list[dict]) -> Iterator[dict]:
|
|
165
155
|
for workbook in workbooks:
|
|
@@ -175,68 +165,6 @@ class SigmaClient(APIClient):
|
|
|
175
165
|
page=page, workbook_id=workbook_id
|
|
176
166
|
)
|
|
177
167
|
|
|
178
|
-
@retry(
|
|
179
|
-
(ConnectionError,),
|
|
180
|
-
max_retries=_RETRY_NUMBER,
|
|
181
|
-
base_ms=_RETRY_BASE_MS,
|
|
182
|
-
log_exc_info=True,
|
|
183
|
-
)
|
|
184
|
-
def _get_lineage(self, lineage_context: LineageContext) -> Lineage:
|
|
185
|
-
"""
|
|
186
|
-
return the lineage from API and other ids needed to characterize
|
|
187
|
-
lineage in castor
|
|
188
|
-
"""
|
|
189
|
-
workbook_id = lineage_context.workbook_id
|
|
190
|
-
element_id = lineage_context.element_id
|
|
191
|
-
endpoint = SigmaEndpointFactory.lineage(workbook_id, element_id)
|
|
192
|
-
return Lineage(lineage=self._get(endpoint), context=lineage_context)
|
|
193
|
-
|
|
194
|
-
@staticmethod
|
|
195
|
-
def _lineage_context(elements: list[dict]) -> list[LineageContext]:
|
|
196
|
-
"""
|
|
197
|
-
Helper function to prepare context for lineage retrieval.
|
|
198
|
-
Elements without associated columns are skipped.
|
|
199
|
-
"""
|
|
200
|
-
contexts: list[LineageContext] = []
|
|
201
|
-
for element in elements:
|
|
202
|
-
if element.get("columns") is None:
|
|
203
|
-
continue
|
|
204
|
-
|
|
205
|
-
context = LineageContext(
|
|
206
|
-
workbook_id=element["workbook_id"],
|
|
207
|
-
element_id=element["elementId"],
|
|
208
|
-
)
|
|
209
|
-
contexts.append(context)
|
|
210
|
-
return contexts
|
|
211
|
-
|
|
212
|
-
def _get_all_lineages(self, elements: list[dict]) -> Iterator[dict]:
|
|
213
|
-
"""
|
|
214
|
-
The safe mode is temporarily modified to include 403 errors.
|
|
215
|
-
|
|
216
|
-
Due to concurrency issues, we force a refresh of the token in hopes that
|
|
217
|
-
the lineage extraction takes less than the token expiration time of
|
|
218
|
-
1 hour.
|
|
219
|
-
"""
|
|
220
|
-
safe_mode = self._safe_mode
|
|
221
|
-
self._safe_mode = SIGMA_SAFE_MODE_LINEAGE
|
|
222
|
-
|
|
223
|
-
lineage_context = self._lineage_context(elements)
|
|
224
|
-
|
|
225
|
-
with ThreadPoolExecutor(max_workers=_THREADS_LINEAGE) as executor:
|
|
226
|
-
results = executor.map(self._get_lineage, lineage_context)
|
|
227
|
-
|
|
228
|
-
for lineage in results:
|
|
229
|
-
if not lineage.lineage:
|
|
230
|
-
continue
|
|
231
|
-
|
|
232
|
-
yield {
|
|
233
|
-
**lineage.lineage,
|
|
234
|
-
"workbook_id": lineage.context.workbook_id,
|
|
235
|
-
"element_id": lineage.context.element_id,
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
self._safe_mode = safe_mode
|
|
239
|
-
|
|
240
168
|
@staticmethod
|
|
241
169
|
def _yield_deduplicated_queries(
|
|
242
170
|
queries: Iterable[dict], workbook_id: str
|
|
@@ -266,6 +194,13 @@ class SigmaClient(APIClient):
|
|
|
266
194
|
|
|
267
195
|
yield from self._yield_deduplicated_queries(queries, workbook_id)
|
|
268
196
|
|
|
197
|
+
def _get_all_datamodel_sources(
|
|
198
|
+
self, datamodels: list[dict]
|
|
199
|
+
) -> Iterator[dict]:
|
|
200
|
+
yield from SigmaSourcesTransformer(
|
|
201
|
+
self, table_id_key="tableId"
|
|
202
|
+
).get_datamodel_sources(datamodels)
|
|
203
|
+
|
|
269
204
|
def _get_all_dataset_sources(self, datasets: list[dict]) -> Iterator[dict]:
|
|
270
205
|
yield from SigmaSourcesTransformer(self).get_dataset_sources(datasets)
|
|
271
206
|
|
|
@@ -277,14 +212,22 @@ class SigmaClient(APIClient):
|
|
|
277
212
|
def fetch(
|
|
278
213
|
self,
|
|
279
214
|
asset: SigmaAsset,
|
|
215
|
+
datamodels: Optional[list[dict]] = None,
|
|
280
216
|
datasets: Optional[list[dict]] = None,
|
|
281
|
-
elements: Optional[list[dict]] = None,
|
|
282
217
|
workbooks: Optional[list[dict]] = None,
|
|
283
218
|
) -> Iterator[dict]:
|
|
284
219
|
"""Returns the needed metadata for the queried asset"""
|
|
285
220
|
if asset == SigmaAsset.DATAMODELS:
|
|
286
221
|
yield from self._get_all_datamodels()
|
|
287
222
|
|
|
223
|
+
elif asset == SigmaAsset.DATAMODEL_SOURCES:
|
|
224
|
+
if datamodels is None:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
"Missing data models to extract data model sources"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
yield from self._get_all_datamodel_sources(datamodels)
|
|
230
|
+
|
|
288
231
|
elif asset == SigmaAsset.DATASETS:
|
|
289
232
|
yield from self._get_all_datasets()
|
|
290
233
|
|
|
@@ -303,12 +246,6 @@ class SigmaClient(APIClient):
|
|
|
303
246
|
elif asset == SigmaAsset.FILES:
|
|
304
247
|
yield from self._get_all_files()
|
|
305
248
|
|
|
306
|
-
elif asset == SigmaAsset.LINEAGES:
|
|
307
|
-
if elements is None:
|
|
308
|
-
raise ValueError("Missing elements to extract lineage")
|
|
309
|
-
|
|
310
|
-
yield from self._get_all_lineages(elements)
|
|
311
|
-
|
|
312
249
|
elif asset == SigmaAsset.MEMBERS:
|
|
313
250
|
yield from self._get_all_members()
|
|
314
251
|
|
|
@@ -19,6 +19,10 @@ class SigmaEndpointFactory:
|
|
|
19
19
|
def datamodels(cls) -> str:
|
|
20
20
|
return f"v2/{cls.DATAMODELS}"
|
|
21
21
|
|
|
22
|
+
@classmethod
|
|
23
|
+
def datamodel_sources(cls, datamodel_id: str) -> str:
|
|
24
|
+
return f"v2/{cls.DATAMODELS}/{datamodel_id}/sources"
|
|
25
|
+
|
|
22
26
|
@classmethod
|
|
23
27
|
def datasets(cls) -> str:
|
|
24
28
|
return f"v2/{cls.DATASETS}"
|
|
@@ -10,7 +10,7 @@ SIGMA_QUERIES_PAGINATION_LIMIT = 50
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SigmaPagination(PaginationModel):
|
|
13
|
-
next_page: Optional[str] =
|
|
13
|
+
next_page: Optional[str] = None
|
|
14
14
|
entries: list = Field(default_factory=list)
|
|
15
15
|
|
|
16
16
|
model_config = ConfigDict(
|
|
@@ -27,3 +27,23 @@ class SigmaPagination(PaginationModel):
|
|
|
27
27
|
|
|
28
28
|
def page_results(self) -> list:
|
|
29
29
|
return self.entries
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SigmaTokenPagination(PaginationModel):
|
|
33
|
+
next_page_token: Optional[str] = "" # noqa: S105
|
|
34
|
+
entries: list = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(
|
|
37
|
+
alias_generator=to_camel,
|
|
38
|
+
populate_by_name=True,
|
|
39
|
+
from_attributes=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def is_last(self) -> bool:
|
|
43
|
+
return not self.next_page_token
|
|
44
|
+
|
|
45
|
+
def next_page_payload(self) -> dict:
|
|
46
|
+
return {"pageToken": self.next_page_token}
|
|
47
|
+
|
|
48
|
+
def page_results(self) -> list:
|
|
49
|
+
return self.entries
|
|
@@ -2,8 +2,9 @@ import logging
|
|
|
2
2
|
from http import HTTPStatus
|
|
3
3
|
from typing import TYPE_CHECKING, Callable, Iterator
|
|
4
4
|
|
|
5
|
-
from ....utils import retry_request
|
|
5
|
+
from ....utils import fetch_all_pages, retry_request
|
|
6
6
|
from .endpoints import SigmaEndpointFactory
|
|
7
|
+
from .pagination import SigmaTokenPagination
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from .client import SigmaClient
|
|
@@ -17,8 +18,11 @@ SIGMA_CONNECTION_PATH_SLEEP_MS = 30_000 # 30 seconds
|
|
|
17
18
|
class SigmaSourcesTransformer:
|
|
18
19
|
"""Retrieves asset sources and enhances them with additional information."""
|
|
19
20
|
|
|
20
|
-
def __init__(
|
|
21
|
+
def __init__(
|
|
22
|
+
self, api_client: "SigmaClient", table_id_key: str = "inodeId"
|
|
23
|
+
):
|
|
21
24
|
self.api_client = api_client
|
|
25
|
+
self.table_id_key = table_id_key
|
|
22
26
|
|
|
23
27
|
@retry_request(
|
|
24
28
|
status_codes=(HTTPStatus.TOO_MANY_REQUESTS,),
|
|
@@ -38,9 +42,9 @@ class SigmaSourcesTransformer:
|
|
|
38
42
|
logger.info("Mapping table ids to connection and path information")
|
|
39
43
|
|
|
40
44
|
unique_table_ids = {
|
|
41
|
-
source[
|
|
45
|
+
source[self.table_id_key]
|
|
42
46
|
for asset_sources in all_sources
|
|
43
|
-
for source in asset_sources
|
|
47
|
+
for source in asset_sources.get("sources", [])
|
|
44
48
|
if source["type"] == "table"
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -49,15 +53,14 @@ class SigmaSourcesTransformer:
|
|
|
49
53
|
for table_id in unique_table_ids
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
def _enhance_table_source(source: dict, table_to_path: dict) -> dict:
|
|
56
|
+
def _enhance_table_source(self, source: dict, table_to_path: dict) -> dict:
|
|
54
57
|
"""
|
|
55
58
|
Combines a single table source with its connection and path information.
|
|
56
59
|
"""
|
|
57
60
|
if source["type"] != "table":
|
|
58
61
|
return source
|
|
59
62
|
|
|
60
|
-
path_info = table_to_path.get(source[
|
|
63
|
+
path_info = table_to_path.get(source[self.table_id_key], {})
|
|
61
64
|
source["connectionId"] = path_info.get("connectionId")
|
|
62
65
|
source["path"] = path_info.get("path")
|
|
63
66
|
return source
|
|
@@ -82,19 +85,35 @@ class SigmaSourcesTransformer:
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
def _get_all_sources(
|
|
85
|
-
self,
|
|
88
|
+
self,
|
|
89
|
+
endpoint: Callable[[str], str],
|
|
90
|
+
asset_ids: set[str],
|
|
91
|
+
with_pagination: bool = False,
|
|
86
92
|
) -> Iterator[dict]:
|
|
87
93
|
"""Returns transformed sources for the given assets"""
|
|
88
94
|
all_sources = []
|
|
89
95
|
|
|
90
96
|
for asset_id in asset_ids:
|
|
91
|
-
|
|
97
|
+
endpoint_url = endpoint(asset_id)
|
|
98
|
+
if with_pagination:
|
|
99
|
+
request = self.api_client._get_paginated(endpoint=endpoint_url)
|
|
100
|
+
sources = list(fetch_all_pages(request, SigmaTokenPagination))
|
|
101
|
+
else:
|
|
102
|
+
sources = self.api_client._get(endpoint=endpoint_url)
|
|
92
103
|
all_sources.append({"asset_id": asset_id, "sources": sources})
|
|
93
104
|
|
|
94
105
|
table_to_path = self._map_table_id_to_connection_path(all_sources)
|
|
95
106
|
|
|
96
107
|
yield from self._transform_sources(all_sources, table_to_path)
|
|
97
108
|
|
|
109
|
+
def get_datamodel_sources(self, datamodels: list[dict]) -> Iterator[dict]:
|
|
110
|
+
asset_ids = {datamodel["dataModelId"] for datamodel in datamodels}
|
|
111
|
+
yield from self._get_all_sources(
|
|
112
|
+
endpoint=SigmaEndpointFactory.datamodel_sources,
|
|
113
|
+
asset_ids=asset_ids,
|
|
114
|
+
with_pagination=True,
|
|
115
|
+
)
|
|
116
|
+
|
|
98
117
|
def get_dataset_sources(self, datasets: list[dict]) -> Iterator[dict]:
|
|
99
118
|
asset_ids = {dataset["datasetId"] for dataset in datasets}
|
|
100
119
|
yield from self._get_all_sources(
|
|
@@ -23,8 +23,14 @@ def iterate_all_data(
|
|
|
23
23
|
"""Iterate over the extracted data from Sigma"""
|
|
24
24
|
|
|
25
25
|
logger.info("Extracting DATA MODELS from API")
|
|
26
|
-
datamodels = client.fetch(SigmaAsset.DATAMODELS)
|
|
27
|
-
yield SigmaAsset.DATASETS,
|
|
26
|
+
datamodels = list(client.fetch(SigmaAsset.DATAMODELS))
|
|
27
|
+
yield SigmaAsset.DATASETS, deep_serialize(datamodels)
|
|
28
|
+
|
|
29
|
+
logger.info("Extracting DATAMODEL SOURCES from API")
|
|
30
|
+
datamodel_sources = client.fetch(
|
|
31
|
+
SigmaAsset.DATAMODEL_SOURCES, datamodels=datamodels
|
|
32
|
+
)
|
|
33
|
+
yield SigmaAsset.DATAMODEL_SOURCES, list(deep_serialize(datamodel_sources))
|
|
28
34
|
|
|
29
35
|
logger.info("Extracting DATASETS from API")
|
|
30
36
|
datasets = list(client.fetch(SigmaAsset.DATASETS))
|
|
@@ -62,10 +68,6 @@ def iterate_all_data(
|
|
|
62
68
|
elements = list(client.fetch(SigmaAsset.ELEMENTS, workbooks=workbooks))
|
|
63
69
|
yield SigmaAsset.ELEMENTS, list(deep_serialize(elements))
|
|
64
70
|
|
|
65
|
-
logging.info("Extracting LINEAGES data from API")
|
|
66
|
-
lineages = client.fetch(SigmaAsset.LINEAGES, elements=elements)
|
|
67
|
-
yield SigmaAsset.LINEAGES, list(deep_serialize(lineages))
|
|
68
|
-
|
|
69
71
|
|
|
70
72
|
def extract_all(**kwargs) -> None:
|
|
71
73
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: castor-extractor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.2
|
|
4
4
|
Summary: Extract your metadata assets.
|
|
5
5
|
Home-page: https://www.castordoc.com/
|
|
6
6
|
License: EULA
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Provides-Extra: all
|
|
18
18
|
Provides-Extra: bigquery
|
|
19
|
+
Provides-Extra: count
|
|
19
20
|
Provides-Extra: databricks
|
|
20
21
|
Provides-Extra: dbt
|
|
21
22
|
Provides-Extra: looker
|
|
@@ -57,7 +58,7 @@ Requires-Dist: setuptools (>=78.1)
|
|
|
57
58
|
Requires-Dist: snowflake-connector-python (>=3.4.0,<4.0.0) ; extra == "snowflake" or extra == "all"
|
|
58
59
|
Requires-Dist: snowflake-sqlalchemy (!=1.2.5,<2.0.0) ; extra == "snowflake" or extra == "all"
|
|
59
60
|
Requires-Dist: sqlalchemy (>=1.4,<1.5)
|
|
60
|
-
Requires-Dist: sqlalchemy-bigquery[bqstorage] (>=1.0.0,<=2.0.0) ; extra == "bigquery" or extra == "all"
|
|
61
|
+
Requires-Dist: sqlalchemy-bigquery[bqstorage] (>=1.0.0,<=2.0.0) ; extra == "bigquery" or extra == "count" or extra == "all"
|
|
61
62
|
Requires-Dist: sqlalchemy-redshift (>=0.8.14,<0.9.0) ; extra == "redshift" or extra == "all"
|
|
62
63
|
Requires-Dist: tableauserverclient (>=0.25.0,<0.26.0) ; extra == "tableau" or extra == "all"
|
|
63
64
|
Requires-Dist: tqdm (>=4.0.0,<5.0.0)
|
|
@@ -215,6 +216,29 @@ For any questions or bug report, contact us at [support@coalesce.io](mailto:supp
|
|
|
215
216
|
|
|
216
217
|
# Changelog
|
|
217
218
|
|
|
219
|
+
## 0.25.2 - 2025-09-30
|
|
220
|
+
|
|
221
|
+
* PowerBi: Support auth with private_key
|
|
222
|
+
|
|
223
|
+
## 0.25.1 - 2025-09-29
|
|
224
|
+
|
|
225
|
+
* Sigma: catch ReadTimeouts during elements extraction
|
|
226
|
+
|
|
227
|
+
## 0.25.0 - 2025-09-15
|
|
228
|
+
|
|
229
|
+
* Count: adding connector
|
|
230
|
+
|
|
231
|
+
## 0.24.57 - 2025-09-24
|
|
232
|
+
|
|
233
|
+
* Sigma:
|
|
234
|
+
* fix pagination
|
|
235
|
+
* remove redundant element lineages endpoint
|
|
236
|
+
* extract data model sources
|
|
237
|
+
|
|
238
|
+
## 0.24.56 - 2025-09-24
|
|
239
|
+
|
|
240
|
+
* bump dependencies
|
|
241
|
+
|
|
218
242
|
## 0.24.55 - 2025-09-19
|
|
219
243
|
|
|
220
244
|
* Fix encoding in LocalStorage - force to utf-8
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
CHANGELOG.md,sha256=
|
|
1
|
+
CHANGELOG.md,sha256=nBloUrrG3Tt7TDnWCZqsNS0x6uIBYG7TFQHoTP8Q8a8,21086
|
|
2
2
|
Dockerfile,sha256=xQ05-CFfGShT3oUqaiumaldwA288dj9Yb_pxofQpufg,301
|
|
3
3
|
DockerfileUsage.md,sha256=2hkJQF-5JuuzfPZ7IOxgM6QgIQW7l-9oRMFVwyXC4gE,998
|
|
4
4
|
LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
|
|
@@ -7,6 +7,7 @@ castor_extractor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
7
7
|
castor_extractor/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
castor_extractor/commands/extract_bigquery.py,sha256=dU4OiYO1V0n32orvZnMh1_xtFKF_VxHNXcVsH3otY-g,1269
|
|
9
9
|
castor_extractor/commands/extract_confluence.py,sha256=blYcnDqywXNKRQ1aZAD9FclhLlO7x8Y_tb0lgl85v0w,1641
|
|
10
|
+
castor_extractor/commands/extract_count.py,sha256=cITp-2UmPYjbcICvYZzxE9oWieI8NbTH1DcWxLAZxJ4,611
|
|
10
11
|
castor_extractor/commands/extract_databricks.py,sha256=SVKyoa-BBUQAM6HRHf1Wdg9-tpICic2yyvXQwHcNBhA,1264
|
|
11
12
|
castor_extractor/commands/extract_domo.py,sha256=jvAawUsUTHrwCn_koK6StmQr4n_b5GyvJi6uu6WS0SM,1061
|
|
12
13
|
castor_extractor/commands/extract_looker.py,sha256=cySLiolLCgrREJ9d0kMrJ7P8K3efHTBTzShalWVfI3A,1214
|
|
@@ -17,7 +18,7 @@ castor_extractor/commands/extract_mode.py,sha256=Q4iO-VAKMg4zFPejhAO-foZibL5Ht3j
|
|
|
17
18
|
castor_extractor/commands/extract_mysql.py,sha256=7AH5qMzeLTsENCOeJwtesrWg8Vo8MCEq8fx2YT74Mcw,1034
|
|
18
19
|
castor_extractor/commands/extract_notion.py,sha256=uaxcF3_bT7D_-JxnIW0F7VVDphI_ZgOfQQxZzoLXo_M,504
|
|
19
20
|
castor_extractor/commands/extract_postgres.py,sha256=pX0RnCPi4nw6QQ6wiAuZ_Xt3ZbDuMUG9aQKuqFgJtAU,1154
|
|
20
|
-
castor_extractor/commands/extract_powerbi.py,sha256=
|
|
21
|
+
castor_extractor/commands/extract_powerbi.py,sha256=tM9fnQaU69zJ7E_uS1S432jprRi9WnpDJdm2NtyLjUg,1242
|
|
21
22
|
castor_extractor/commands/extract_qlik.py,sha256=VBe_xFKh_nR0QSFFIncAaC8yDqBeMa6VunBAga7AeGg,891
|
|
22
23
|
castor_extractor/commands/extract_redshift.py,sha256=zRBg2D_ft4GLdPSdmetRcgQVAA80DXtdRSYsQhAWIik,1334
|
|
23
24
|
castor_extractor/commands/extract_salesforce.py,sha256=3j3YTmMkPAwocR-B1ozJQai0UIZPtpmAyWj-hHvdWn4,1226
|
|
@@ -160,6 +161,17 @@ castor_extractor/utils/validation.py,sha256=dRvC9SoFVecVZuLQNN3URq37yX2sBSW3-NxI
|
|
|
160
161
|
castor_extractor/utils/validation_test.py,sha256=A7P6VmI0kYX2aGIeEN12y7LsY7Kpm8pE4bdVFhbBAMw,1184
|
|
161
162
|
castor_extractor/utils/write.py,sha256=KQVWF29N766avzmSb129IUWrId5c_8BtnYhVLmU6YIs,2133
|
|
162
163
|
castor_extractor/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
164
|
+
castor_extractor/visualization/count/__init__.py,sha256=lvxGtSe3erjTYK0aPnkOyJibcsC6Q1AFchnK-hZt558,114
|
|
165
|
+
castor_extractor/visualization/count/assets.py,sha256=VZCRVDKWSu6l2lVGJS4JKOOmfCUkbS8MnJiLcAY9vqw,232
|
|
166
|
+
castor_extractor/visualization/count/client/__init__.py,sha256=YawYDutDI0sprp72jN9tKi8bbXCoc0Ij0Ev582tKjqk,74
|
|
167
|
+
castor_extractor/visualization/count/client/client.py,sha256=WgljCj8G7D0Brxa0llaeOQ2Ipd7FvtDWFoLWoPyqT9A,1523
|
|
168
|
+
castor_extractor/visualization/count/client/credentials.py,sha256=LZWvcz7p5lrgdgoIQLcxFyv4gqUBW4Jj4qDKN-VW31I,273
|
|
169
|
+
castor_extractor/visualization/count/client/queries/canvas_permissions.sql,sha256=iFmMfR0zusjxTxmYUS6p0kibZCsnHOQMbAlxaNjx-H4,108
|
|
170
|
+
castor_extractor/visualization/count/client/queries/canvases.sql,sha256=Ur5HBD9JJH0r14xIj_rwoctnds082_F931vlfcnwi_I,86
|
|
171
|
+
castor_extractor/visualization/count/client/queries/cells.sql,sha256=Kkk0jyU337PD6RPshSo_ucLl5PS7kIvJZlUnVnmJUkM,111
|
|
172
|
+
castor_extractor/visualization/count/client/queries/projects.sql,sha256=3Jem3QCVwk4wHiWRJL7cN6Vl2Yc5RZ8yC8ndvPAkaFM,68
|
|
173
|
+
castor_extractor/visualization/count/client/queries/users.sql,sha256=H0n7S7P5cCAWbgPxU32psIc1epXySzsAaQ7MQ9JrkfM,102
|
|
174
|
+
castor_extractor/visualization/count/extract.py,sha256=ZBsJ9tMxxaq1jG8qJp_OGVK3yPDNkVUsP1_3rcUMtYg,1378
|
|
163
175
|
castor_extractor/visualization/domo/__init__.py,sha256=1axOCPm4RpdIyUt9LQEvlMvbOPllW8rk63h6EjVgJ0Y,111
|
|
164
176
|
castor_extractor/visualization/domo/assets.py,sha256=bK1urFR2tnlWkVkkhR32mAKMoKbESNlop-CNGx-65PY,206
|
|
165
177
|
castor_extractor/visualization/domo/client/__init__.py,sha256=Do0fU4B8Hhlhahcv734gnJl_ryCztfTBDea7XNCKfB8,72
|
|
@@ -236,16 +248,16 @@ castor_extractor/visualization/mode/errors.py,sha256=SKpFT2AiLOuWx2VRLyO7jbAiKcG
|
|
|
236
248
|
castor_extractor/visualization/mode/extract.py,sha256=PmLWWjUwplQh3TNMemiGwyFdxMcKVMvumZPxSMLJAwk,1625
|
|
237
249
|
castor_extractor/visualization/powerbi/__init__.py,sha256=hoZ73ngLhMc9edqxO9PUIE3FABQlvcfY2W8fuc6DEjY,197
|
|
238
250
|
castor_extractor/visualization/powerbi/assets.py,sha256=IB_XKwgdN1pZYGZ4RfeHrLjflianTzWf_6tg-4CIwu0,742
|
|
239
|
-
castor_extractor/visualization/powerbi/client/__init__.py,sha256=
|
|
240
|
-
castor_extractor/visualization/powerbi/client/authentication.py,sha256=
|
|
251
|
+
castor_extractor/visualization/powerbi/client/__init__.py,sha256=rxWeAtmGsy1XYn2oIrGz5rIlxcTrzh2rl1V-MGxFOY4,175
|
|
252
|
+
castor_extractor/visualization/powerbi/client/authentication.py,sha256=1pST-w7ceqrcKSccQSJBxT4lAsLU8keceSVJro1dg8k,1516
|
|
241
253
|
castor_extractor/visualization/powerbi/client/client.py,sha256=Q_WHYGFpHT4wJ6nZvJa96nBVcpUGv7E2WnyZHBftsJM,8340
|
|
242
254
|
castor_extractor/visualization/powerbi/client/client_test.py,sha256=zWgfc8fOHSRn3hxiX8ujJysmNHeypIoKin9h8_h178k,6668
|
|
243
255
|
castor_extractor/visualization/powerbi/client/constants.py,sha256=88R_aGachNNUZh6OSH2fkDwZtY4KTStzKm_g7HNCqqo,387
|
|
244
|
-
castor_extractor/visualization/powerbi/client/credentials.py,sha256=
|
|
256
|
+
castor_extractor/visualization/powerbi/client/credentials.py,sha256=Mqb9e9jbJrawE00xvLyej1i4tFM8VNiRnA0LpfqORd0,1565
|
|
245
257
|
castor_extractor/visualization/powerbi/client/credentials_test.py,sha256=TzFqxsWVQ3sXR_n0bJsexK9Uz7ceXCEPVqDGWTJzW60,993
|
|
246
258
|
castor_extractor/visualization/powerbi/client/endpoints.py,sha256=38ZETzSSnNq3vA9O6nLZQ8T1BVE01R9CjMC03-PRXsM,1911
|
|
247
259
|
castor_extractor/visualization/powerbi/client/pagination.py,sha256=OZMjoDQPRGMoWd9QcKKrPh3aErJR20SHlrTqY_siLkk,755
|
|
248
|
-
castor_extractor/visualization/powerbi/extract.py,sha256=
|
|
260
|
+
castor_extractor/visualization/powerbi/extract.py,sha256=bZOUbciWGPNRRrtcMezSdoeClHB2yiBATBC8UqoXz5M,1904
|
|
249
261
|
castor_extractor/visualization/qlik/__init__.py,sha256=u6lIfm_WOykBwt6SlaB7C0Dtx37XBliUbM5oWv26gC8,177
|
|
250
262
|
castor_extractor/visualization/qlik/assets.py,sha256=Ab_kG61mHcK8GoGZbfQW7RSWyd7D9bVga9DOqnm0iSE,1625
|
|
251
263
|
castor_extractor/visualization/qlik/client/__init__.py,sha256=5O5N9Jrt3d99agFEJ28lKWs2KkDaXK-lZ07IUtLj56M,130
|
|
@@ -270,17 +282,17 @@ castor_extractor/visualization/salesforce_reporting/client/rest.py,sha256=AqL1DT
|
|
|
270
282
|
castor_extractor/visualization/salesforce_reporting/client/soql.py,sha256=ytZnX6zE-NoS_Kz12KghMcCM4ukPwhMj6U0rQZ_8Isk,1621
|
|
271
283
|
castor_extractor/visualization/salesforce_reporting/extract.py,sha256=ScStilebLGf4HDTFqhVTQAvv_OrKxc8waycfBKdsVAc,1359
|
|
272
284
|
castor_extractor/visualization/sigma/__init__.py,sha256=GINql4yJLtjfOJgjHaWNpE13cMtnKNytiFRomwav27Q,114
|
|
273
|
-
castor_extractor/visualization/sigma/assets.py,sha256=
|
|
285
|
+
castor_extractor/visualization/sigma/assets.py,sha256=iVZqi7XtNgSOVXy0jgeHZonVOeXi7jyikor8ztbECBc,398
|
|
274
286
|
castor_extractor/visualization/sigma/client/__init__.py,sha256=YQv06FBBQHvBMFg_tN0nUcmUp2NCL2s-eFTXG8rXaBg,74
|
|
275
287
|
castor_extractor/visualization/sigma/client/authentication.py,sha256=gHukrpfboIjZc_O9CcuDtrl6U-StH0J73VY2J74Bm9o,2279
|
|
276
|
-
castor_extractor/visualization/sigma/client/client.py,sha256=
|
|
288
|
+
castor_extractor/visualization/sigma/client/client.py,sha256=SxSf5OjdDr8x-WZDezm8YNOw01R6CCoYIgW0od0ZgN8,8907
|
|
277
289
|
castor_extractor/visualization/sigma/client/client_test.py,sha256=ae0ZOvKutCm44jnrJ-0_A5Y6ZGyDkMf9Ml3eEP8dNkY,581
|
|
278
290
|
castor_extractor/visualization/sigma/client/credentials.py,sha256=XddAuQSmCKpxJ70TQgRnOj0vMPYVtiStk_lMMQ1AiNM,693
|
|
279
|
-
castor_extractor/visualization/sigma/client/endpoints.py,sha256=
|
|
280
|
-
castor_extractor/visualization/sigma/client/pagination.py,sha256=
|
|
281
|
-
castor_extractor/visualization/sigma/client/sources_transformer.py,sha256=
|
|
291
|
+
castor_extractor/visualization/sigma/client/endpoints.py,sha256=by9VIFml2whlzQT66f2m56RYBsqPrWdAmIP4JkTaBV4,1799
|
|
292
|
+
castor_extractor/visualization/sigma/client/pagination.py,sha256=9kCYQpO7hAH2qvYmnVjnGVUDLkpkEM6BgYlv-JTY8AE,1241
|
|
293
|
+
castor_extractor/visualization/sigma/client/sources_transformer.py,sha256=2f7REl70wYitopftMtYQU-E8kISVck67i7rGYgf3tkk,4552
|
|
282
294
|
castor_extractor/visualization/sigma/client/sources_transformer_test.py,sha256=06yUHXyv65amXLKXhix6K3kkVc1kpBqSjIYcxbyMI4Y,2766
|
|
283
|
-
castor_extractor/visualization/sigma/extract.py,sha256=
|
|
295
|
+
castor_extractor/visualization/sigma/extract.py,sha256=iRmRUzSnq_ObG9fxpOI5Rs07EKKT-VRLcyiti5-8D4c,2986
|
|
284
296
|
castor_extractor/visualization/strategy/__init__.py,sha256=HOMv4JxqF5ZmViWi-pDE-PSXJRLTdXal_jtpHG_rlR8,123
|
|
285
297
|
castor_extractor/visualization/strategy/assets.py,sha256=yFXF_dX01patC0HQ1eU7Jo_4DZ4m6IJEg0uCB71tMoI,480
|
|
286
298
|
castor_extractor/visualization/strategy/client/__init__.py,sha256=XWP0yF5j6JefDJkDfX-RSJn3HF2ceQ0Yx1PLCfB3BBo,80
|
|
@@ -434,8 +446,8 @@ castor_extractor/warehouse/sqlserver/queries/user.sql,sha256=MAlnTis43E3Amu1e1Oz
|
|
|
434
446
|
castor_extractor/warehouse/sqlserver/queries/view_ddl.sql,sha256=9rynvx6MWg3iZzrWPB7haZfVKEPkxulzryE2g19x804,315
|
|
435
447
|
castor_extractor/warehouse/sqlserver/query.py,sha256=c8f7_SEMR17DhbtzuYphWqWDQ0sCRy-nR442RRBZVYw,1773
|
|
436
448
|
castor_extractor/warehouse/synapse/queries/column.sql,sha256=lNcFoIW3Y0PFOqoOzJEXmPvZvfAsY0AP63Mu2LuPzPo,1351
|
|
437
|
-
castor_extractor-0.
|
|
438
|
-
castor_extractor-0.
|
|
439
|
-
castor_extractor-0.
|
|
440
|
-
castor_extractor-0.
|
|
441
|
-
castor_extractor-0.
|
|
449
|
+
castor_extractor-0.25.2.dist-info/LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
|
|
450
|
+
castor_extractor-0.25.2.dist-info/METADATA,sha256=Lh6TLvQYvBJ0wL4ST5GXkpGX4DUaZzNsThF9ZiBCOzk,28588
|
|
451
|
+
castor_extractor-0.25.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
452
|
+
castor_extractor-0.25.2.dist-info/entry_points.txt,sha256=qyTrKNByoq2HYi1xbA79OU7qxg-OWPvle8VwDqt-KnE,1869
|
|
453
|
+
castor_extractor-0.25.2.dist-info/RECORD,,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[console_scripts]
|
|
2
2
|
castor-extract-bigquery=castor_extractor.commands.extract_bigquery:main
|
|
3
3
|
castor-extract-confluence=castor_extractor.commands.extract_confluence:main
|
|
4
|
+
castor-extract-count=castor_extractor.commands.extract_count:main
|
|
4
5
|
castor-extract-databricks=castor_extractor.commands.extract_databricks:main
|
|
5
6
|
castor-extract-domo=castor_extractor.commands.extract_domo:main
|
|
6
7
|
castor-extract-looker=castor_extractor.commands.extract_looker:main
|
|
File without changes
|
|
File without changes
|