castor-extractor 0.17.4__py3-none-any.whl → 0.18.5__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 +28 -0
- DockerfileUsage.md +21 -0
- castor_extractor/commands/extract_domo.py +2 -10
- castor_extractor/commands/extract_looker.py +2 -13
- castor_extractor/commands/extract_metabase_api.py +5 -10
- castor_extractor/commands/extract_metabase_db.py +6 -16
- castor_extractor/commands/extract_mode.py +2 -13
- castor_extractor/commands/extract_powerbi.py +2 -8
- castor_extractor/commands/extract_qlik.py +2 -7
- castor_extractor/commands/extract_salesforce.py +3 -12
- castor_extractor/commands/extract_salesforce_reporting.py +2 -10
- castor_extractor/commands/extract_sigma.py +2 -7
- castor_extractor/utils/__init__.py +3 -1
- castor_extractor/utils/argument_parser.py +7 -0
- castor_extractor/utils/argument_parser_test.py +25 -0
- castor_extractor/utils/collection.py +8 -0
- castor_extractor/utils/safe_request.py +57 -0
- castor_extractor/utils/safe_request_test.py +77 -0
- castor_extractor/utils/salesforce/__init__.py +1 -2
- castor_extractor/utils/salesforce/constants.py +0 -11
- castor_extractor/utils/salesforce/credentials.py +22 -45
- castor_extractor/visualization/domo/__init__.py +1 -1
- castor_extractor/visualization/domo/client/__init__.py +1 -1
- castor_extractor/visualization/domo/client/client.py +37 -52
- castor_extractor/visualization/domo/client/credentials.py +14 -27
- castor_extractor/visualization/domo/extract.py +5 -26
- castor_extractor/visualization/looker/__init__.py +6 -1
- castor_extractor/visualization/looker/api/__init__.py +2 -1
- castor_extractor/visualization/looker/api/client.py +6 -4
- castor_extractor/visualization/looker/api/client_test.py +5 -3
- castor_extractor/visualization/looker/api/credentials.py +33 -0
- castor_extractor/visualization/looker/api/extraction_parameters.py +38 -0
- castor_extractor/visualization/looker/api/sdk.py +2 -28
- castor_extractor/visualization/looker/constant.py +2 -27
- castor_extractor/visualization/looker/constants.py +17 -0
- castor_extractor/visualization/looker/extract.py +29 -29
- castor_extractor/visualization/metabase/__init__.py +6 -1
- castor_extractor/visualization/metabase/client/__init__.py +2 -2
- castor_extractor/visualization/metabase/client/api/__init__.py +1 -0
- castor_extractor/visualization/metabase/client/api/client.py +8 -14
- castor_extractor/visualization/metabase/client/api/credentials.py +13 -40
- castor_extractor/visualization/metabase/client/db/__init__.py +1 -0
- castor_extractor/visualization/metabase/client/db/client.py +13 -34
- castor_extractor/visualization/metabase/client/db/credentials.py +19 -73
- castor_extractor/visualization/metabase/errors.py +5 -3
- castor_extractor/visualization/metabase/extract.py +3 -3
- castor_extractor/visualization/mode/__init__.py +1 -1
- castor_extractor/visualization/mode/client/__init__.py +1 -0
- castor_extractor/visualization/mode/client/client.py +9 -12
- castor_extractor/visualization/mode/client/client_test.py +3 -3
- castor_extractor/visualization/mode/client/credentials.py +18 -51
- castor_extractor/visualization/mode/extract.py +6 -3
- castor_extractor/visualization/powerbi/__init__.py +1 -1
- castor_extractor/visualization/powerbi/client/__init__.py +2 -1
- castor_extractor/visualization/powerbi/client/credentials.py +17 -9
- castor_extractor/visualization/powerbi/client/credentials_test.py +12 -4
- castor_extractor/visualization/powerbi/client/rest.py +2 -2
- castor_extractor/visualization/powerbi/client/rest_test.py +2 -2
- castor_extractor/visualization/powerbi/extract.py +5 -16
- castor_extractor/visualization/qlik/__init__.py +5 -1
- castor_extractor/visualization/qlik/client/__init__.py +1 -0
- castor_extractor/visualization/qlik/client/engine/__init__.py +1 -0
- castor_extractor/visualization/qlik/client/engine/client.py +5 -6
- castor_extractor/visualization/qlik/client/engine/credentials.py +26 -0
- castor_extractor/visualization/qlik/client/master.py +5 -11
- castor_extractor/visualization/qlik/client/rest.py +4 -4
- castor_extractor/visualization/qlik/client/rest_test.py +6 -2
- castor_extractor/visualization/qlik/extract.py +6 -13
- castor_extractor/visualization/salesforce_reporting/extract.py +6 -20
- castor_extractor/visualization/sigma/__init__.py +1 -1
- castor_extractor/visualization/sigma/client/__init__.py +1 -1
- castor_extractor/visualization/sigma/client/client.py +5 -4
- castor_extractor/visualization/sigma/client/credentials.py +12 -28
- castor_extractor/visualization/sigma/extract.py +5 -18
- castor_extractor/visualization/tableau_revamp/client/credentials.py +40 -87
- castor_extractor/warehouse/databricks/client.py +3 -0
- castor_extractor/warehouse/redshift/queries/column.sql +0 -5
- castor_extractor/warehouse/salesforce/extract.py +2 -2
- castor_extractor/warehouse/salesforce/format.py +5 -3
- castor_extractor/warehouse/snowflake/queries/column.sql +0 -1
- castor_extractor/warehouse/synapse/queries/column.sql +0 -1
- {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/METADATA +9 -9
- {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/RECORD +86 -83
- castor_extractor/visualization/domo/client/client_test.py +0 -60
- castor_extractor/visualization/domo/constants.py +0 -6
- castor_extractor/visualization/looker/env.py +0 -48
- castor_extractor/visualization/looker/parameters.py +0 -78
- castor_extractor/visualization/qlik/constants.py +0 -3
- castor_extractor/visualization/sigma/constants.py +0 -4
- {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/LICENCE +0 -0
- {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/WHEEL +0 -0
- {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/entry_points.txt +0 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.5 - 2024-07-17
|
|
4
|
+
|
|
5
|
+
* Salesforce: extract DeveloperName and tooling url
|
|
6
|
+
|
|
7
|
+
## 0.18.4 - 2024-07-16
|
|
8
|
+
|
|
9
|
+
* Fix environment variables assignments for credentials
|
|
10
|
+
|
|
11
|
+
## 0.18.3 - 2024-07-16
|
|
12
|
+
|
|
13
|
+
* bump dependencies (minor and patches)
|
|
14
|
+
|
|
15
|
+
## 0.18.2 - 2024-07-08
|
|
16
|
+
|
|
17
|
+
* Added StatusCode handling to SafeMode
|
|
18
|
+
|
|
19
|
+
## 0.18.1 - 2024-07-04
|
|
20
|
+
|
|
21
|
+
* Bump dependencies: numpy, setuptools, tableauserverclient
|
|
22
|
+
|
|
23
|
+
## 0.18.0 - 2024-07-03
|
|
24
|
+
|
|
25
|
+
* Dashboarding technologies : Reworked credentials using Pydantic
|
|
26
|
+
|
|
27
|
+
## 0.17.5 - 2024-07-03
|
|
28
|
+
|
|
29
|
+
* Snowflake, Synapse, Redshift: Remove default_value from the extracted column
|
|
30
|
+
|
|
3
31
|
## 0.17.4 - 2024-07-03
|
|
4
32
|
|
|
5
33
|
* Sigma: Add `input-table`, `pivot-table` and `viz` in the list of supported **Elements**
|
DockerfileUsage.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Dockerfile usage for CastorDoc package
|
|
2
|
+
|
|
3
|
+
## How To
|
|
4
|
+
|
|
5
|
+
- The Dockerfile is present on the pypi package
|
|
6
|
+
- For building it you should use this command `docker build -t castor-extractor-looker --build-arg EXTRA=looker .` with replacing looker one or several of: [bigquery,looker,metabase,powerbi,qlik,redshift,snowflake,tableau]
|
|
7
|
+
- For running it you should do `docker run -v <local-path>:/data --env-file <castor-extract-looker.env> castor-extractor-looker` where `</local-path>` have to be replaced and `<castor-extract-looker.env>` have to be set.
|
|
8
|
+
- Extracted datas would be available on `<local-path>`. The path should exists
|
|
9
|
+
- `<castor-extract-looker.env>` would contain env vars for credentials, url...
|
|
10
|
+
|
|
11
|
+
#### example
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
docker run -v /logs:/data --env-file /config/castor-extract-looker.env castor-extractor-looker
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Limitation
|
|
18
|
+
|
|
19
|
+
- This docker image is for a specific techno
|
|
20
|
+
- This docker image is based on python 3.11
|
|
21
|
+
- This docker image use the latest castor-extractor package version
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import domo # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -38,13 +39,4 @@ def main():
|
|
|
38
39
|
|
|
39
40
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
domo.extract_all(
|
|
44
|
-
api_token=args.api_token,
|
|
45
|
-
base_url=args.base_url,
|
|
46
|
-
client_id=args.client_id,
|
|
47
|
-
cloud_id=args.cloud_id,
|
|
48
|
-
developer_token=args.developer_token,
|
|
49
|
-
output_directory=args.output,
|
|
50
|
-
)
|
|
42
|
+
domo.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from argparse import ArgumentParser
|
|
2
2
|
|
|
3
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
3
4
|
from castor_extractor.visualization import looker # type: ignore
|
|
4
5
|
|
|
5
6
|
|
|
@@ -33,16 +34,4 @@ def main():
|
|
|
33
34
|
action="store_true",
|
|
34
35
|
)
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
looker.extract_all(
|
|
39
|
-
base_url=args.base_url,
|
|
40
|
-
client_id=args.username,
|
|
41
|
-
client_secret=args.password,
|
|
42
|
-
log_to_stdout=args.log_to_stdout,
|
|
43
|
-
output_directory=args.output,
|
|
44
|
-
safe_mode=args.safe_mode,
|
|
45
|
-
search_per_folder=args.search_per_folder,
|
|
46
|
-
thread_pool_size=args.thread_pool_size,
|
|
47
|
-
timeout=args.timeout,
|
|
48
|
-
)
|
|
37
|
+
looker.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import metabase # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -15,15 +16,9 @@ def main():
|
|
|
15
16
|
|
|
16
17
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
17
18
|
|
|
18
|
-
args = parser
|
|
19
|
+
args = parse_filled_arguments(parser)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
user=args.username,
|
|
23
|
-
password=args.password,
|
|
24
|
-
)
|
|
21
|
+
credentials = metabase.MetabaseApiCredentials(**args)
|
|
22
|
+
client = metabase.ApiClient(credentials)
|
|
25
23
|
|
|
26
|
-
metabase.extract_all(
|
|
27
|
-
client,
|
|
28
|
-
output_directory=args.output,
|
|
29
|
-
)
|
|
24
|
+
metabase.extract_all(client, **args)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import metabase # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -33,20 +34,9 @@ def main():
|
|
|
33
34
|
|
|
34
35
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
35
36
|
|
|
36
|
-
args = parser
|
|
37
|
-
|
|
38
|
-
client = metabase.DbClient(
|
|
39
|
-
host=args.host,
|
|
40
|
-
port=args.port,
|
|
41
|
-
database=args.database,
|
|
42
|
-
schema=args.schema,
|
|
43
|
-
user=args.username,
|
|
44
|
-
password=args.password,
|
|
45
|
-
encryption_secret_key=args.encryption_secret_key,
|
|
46
|
-
require_ssl=args.require_ssl,
|
|
47
|
-
)
|
|
37
|
+
args = parse_filled_arguments(parser)
|
|
38
|
+
credentials = metabase.MetabaseDbCredentials(**args)
|
|
48
39
|
|
|
49
|
-
metabase.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
40
|
+
client = metabase.DbClient(credentials)
|
|
41
|
+
|
|
42
|
+
metabase.extract_all(client, **args)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import mode # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -24,16 +25,4 @@ def main():
|
|
|
24
25
|
|
|
25
26
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
client = mode.Client(
|
|
30
|
-
host=args.host,
|
|
31
|
-
workspace=args.workspace,
|
|
32
|
-
token=args.token,
|
|
33
|
-
secret=args.secret,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
mode.extract_all(
|
|
37
|
-
client,
|
|
38
|
-
output_directory=args.output,
|
|
39
|
-
)
|
|
28
|
+
mode.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import powerbi # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -20,11 +21,4 @@ def main():
|
|
|
20
21
|
)
|
|
21
22
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
powerbi.extract_all(
|
|
25
|
-
tenant_id=args.tenant_id,
|
|
26
|
-
client_id=args.client_id,
|
|
27
|
-
secret=args.secret,
|
|
28
|
-
scopes=args.scopes,
|
|
29
|
-
output_directory=args.output,
|
|
30
|
-
)
|
|
24
|
+
powerbi.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import qlik # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -22,10 +23,4 @@ def main():
|
|
|
22
23
|
"missing rights on some assets.",
|
|
23
24
|
)
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
qlik.extract_all(
|
|
27
|
-
base_url=args.base_url,
|
|
28
|
-
api_key=args.api_key,
|
|
29
|
-
output_directory=args.output,
|
|
30
|
-
except_http_error_statuses=args.except_http_error_statuses,
|
|
31
|
-
)
|
|
26
|
+
qlik.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.warehouse import salesforce # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -29,15 +30,5 @@ def main():
|
|
|
29
30
|
)
|
|
30
31
|
parser.set_defaults(skip_existing=False)
|
|
31
32
|
|
|
32
|
-
args = parser
|
|
33
|
-
|
|
34
|
-
salesforce.extract_all(
|
|
35
|
-
username=args.username,
|
|
36
|
-
password=args.password,
|
|
37
|
-
client_id=args.client_id,
|
|
38
|
-
client_secret=args.client_secret,
|
|
39
|
-
security_token=args.security_token,
|
|
40
|
-
base_url=args.base_url,
|
|
41
|
-
output_directory=args.output,
|
|
42
|
-
skip_existing=args.skip_existing,
|
|
43
|
-
)
|
|
33
|
+
args = parse_filled_arguments(parser)
|
|
34
|
+
salesforce.extract_all(output_directory=args.get("output"), **args)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import salesforce_reporting # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -21,13 +22,4 @@ def main():
|
|
|
21
22
|
parser.add_argument("-b", "--base-url", help="Salesforce instance URL")
|
|
22
23
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
salesforce_reporting.extract_all(
|
|
26
|
-
username=args.username,
|
|
27
|
-
password=args.password,
|
|
28
|
-
client_id=args.client_id,
|
|
29
|
-
client_secret=args.client_secret,
|
|
30
|
-
security_token=args.security_token,
|
|
31
|
-
base_url=args.base_url,
|
|
32
|
-
output_directory=args.output,
|
|
33
|
-
)
|
|
25
|
+
salesforce_reporting.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from argparse import ArgumentParser
|
|
3
3
|
|
|
4
|
+
from castor_extractor.utils import parse_filled_arguments # type: ignore
|
|
4
5
|
from castor_extractor.visualization import sigma # type: ignore
|
|
5
6
|
|
|
6
7
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
|
|
@@ -14,10 +15,4 @@ def main():
|
|
|
14
15
|
parser.add_argument("-a", "--api-token", help="Generated API key")
|
|
15
16
|
parser.add_argument("-o", "--output", help="Directory to write to")
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
sigma.extract_all(
|
|
19
|
-
host=args.host,
|
|
20
|
-
client_id=args.client_id,
|
|
21
|
-
api_token=args.api_token,
|
|
22
|
-
output_directory=args.output,
|
|
23
|
-
)
|
|
18
|
+
sigma.extract_all(**parse_filled_arguments(parser))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .argument_parser import parse_filled_arguments
|
|
1
2
|
from .client import (
|
|
2
3
|
AbstractSourceClient,
|
|
3
4
|
ExtractionQuery,
|
|
@@ -5,7 +6,7 @@ from .client import (
|
|
|
5
6
|
SqlalchemyClient,
|
|
6
7
|
uri_encode,
|
|
7
8
|
)
|
|
8
|
-
from .collection import group_by, mapping_from_rows
|
|
9
|
+
from .collection import empty_iterator, group_by, mapping_from_rows
|
|
9
10
|
from .constants import OUTPUT_DIR
|
|
10
11
|
from .deprecate import deprecate_python
|
|
11
12
|
from .env import from_env
|
|
@@ -23,6 +24,7 @@ from .pager import (
|
|
|
23
24
|
)
|
|
24
25
|
from .retry import RetryStrategy, retry
|
|
25
26
|
from .safe import SafeMode, safe_mode
|
|
27
|
+
from .safe_request import RequestSafeMode, ResponseJson, handle_response
|
|
26
28
|
from .store import AbstractStorage, LocalStorage
|
|
27
29
|
from .string import decode_when_bytes, string_to_tuple
|
|
28
30
|
from .time import (
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_filled_arguments(parser: ArgumentParser) -> dict:
|
|
5
|
+
"""Parse arguments and remove all those with None values"""
|
|
6
|
+
parsed_arguments = vars(parser.parse_args())
|
|
7
|
+
return {k: v for k, v in parsed_arguments.items() if v is not None}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from argparse import Namespace
|
|
2
|
+
|
|
3
|
+
from .argument_parser import parse_filled_arguments
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MockArgumentParser:
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.attributes = {}
|
|
10
|
+
|
|
11
|
+
def add_argument(self, name, value):
|
|
12
|
+
self.attributes[name] = value
|
|
13
|
+
|
|
14
|
+
def parse_args(self) -> Namespace:
|
|
15
|
+
return Namespace(**self.attributes)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_filled_arguments():
|
|
19
|
+
parser = MockArgumentParser()
|
|
20
|
+
parser.add_argument("filled", "value")
|
|
21
|
+
parser.add_argument("unfilled", None)
|
|
22
|
+
parser.add_argument("empty_str", "")
|
|
23
|
+
|
|
24
|
+
expected = {"filled": "value", "empty_str": ""}
|
|
25
|
+
assert parse_filled_arguments(parser) == expected
|
|
@@ -44,3 +44,11 @@ def mapping_from_rows(rows: List[Dict], key: Any, value: Any) -> Dict:
|
|
|
44
44
|
mapping[mapping_key] = mapping_value
|
|
45
45
|
|
|
46
46
|
return mapping
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def empty_iterator():
|
|
50
|
+
"""
|
|
51
|
+
Utils to return empty iterator, mainly used for viz transformers
|
|
52
|
+
Remark: missing return type is on purpose, it breaks the typing
|
|
53
|
+
"""
|
|
54
|
+
return iter([])
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from requests import HTTPError, Response
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
ResponseJson = Union[dict, List[dict]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RequestSafeMode:
|
|
12
|
+
"""
|
|
13
|
+
RequestSafeMode class to parameterize what should be done if response
|
|
14
|
+
raises due to the status code.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
self.status_codes: tuple of status codes that will be caught
|
|
18
|
+
self.errors_caught : list of errors caught
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
max_errors: Union[int, float] = 0,
|
|
24
|
+
status_codes: Tuple[int, ...] = (),
|
|
25
|
+
):
|
|
26
|
+
self.max_errors = max_errors
|
|
27
|
+
self.status_codes: List[int] = list(status_codes)
|
|
28
|
+
self.status_codes_caught: List[int] = []
|
|
29
|
+
|
|
30
|
+
def catch_response(self, exception: HTTPError, status_code: int):
|
|
31
|
+
if int(status_code) not in self.status_codes:
|
|
32
|
+
raise exception
|
|
33
|
+
|
|
34
|
+
self.status_codes_caught.append(int(status_code))
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def should_raise(self) -> bool:
|
|
38
|
+
return len(self.status_codes_caught) > self.max_errors
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def handle_response(
|
|
42
|
+
response: Response, safe_mode: RequestSafeMode
|
|
43
|
+
) -> ResponseJson:
|
|
44
|
+
"""
|
|
45
|
+
Util to handle a HTTP Response based on the response status code and the
|
|
46
|
+
safe mode used
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
except HTTPError as e:
|
|
51
|
+
safe_mode.catch_response(e, response.status_code)
|
|
52
|
+
if safe_mode.should_raise:
|
|
53
|
+
raise e
|
|
54
|
+
logger.error(f"Safe mode : skip request with error {e}")
|
|
55
|
+
logger.debug(e, exc_info=True)
|
|
56
|
+
return {}
|
|
57
|
+
return response.json()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from requests import HTTPError, Response
|
|
6
|
+
|
|
7
|
+
from .safe_request import RequestSafeMode, handle_response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def mock_response(status_code: int):
|
|
11
|
+
response = Response()
|
|
12
|
+
response.status_code = status_code
|
|
13
|
+
response.raw = io.BytesIO(b'[{"data": "working"}]')
|
|
14
|
+
return response
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_http_error_with_no_safe_mode():
|
|
18
|
+
safe_params = RequestSafeMode() # Caught
|
|
19
|
+
|
|
20
|
+
with pytest.raises(HTTPError):
|
|
21
|
+
handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_http_error_with_no_status_code():
|
|
25
|
+
safe_params = RequestSafeMode(2) # Caught
|
|
26
|
+
|
|
27
|
+
with pytest.raises(HTTPError):
|
|
28
|
+
handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_http_error_with_status_code():
|
|
32
|
+
safe_params = RequestSafeMode(2, (HTTPStatus.FORBIDDEN,)) # Caught
|
|
33
|
+
|
|
34
|
+
def call():
|
|
35
|
+
return handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
|
|
36
|
+
|
|
37
|
+
assert call() == {}
|
|
38
|
+
assert call() == {}
|
|
39
|
+
|
|
40
|
+
with pytest.raises(HTTPError):
|
|
41
|
+
call()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_http_error_with_multiple_status_code():
|
|
45
|
+
safe_params = RequestSafeMode(
|
|
46
|
+
2, (HTTPStatus.NOT_FOUND, HTTPStatus.FORBIDDEN)
|
|
47
|
+
) # Caught
|
|
48
|
+
|
|
49
|
+
def call():
|
|
50
|
+
return handle_response(mock_response(HTTPStatus.FORBIDDEN), safe_params)
|
|
51
|
+
|
|
52
|
+
def call_2():
|
|
53
|
+
return handle_response(mock_response(HTTPStatus.NOT_FOUND), safe_params)
|
|
54
|
+
|
|
55
|
+
assert call() == {}
|
|
56
|
+
assert call_2() == {}
|
|
57
|
+
with pytest.raises(HTTPError): # 3 failed calls > retries
|
|
58
|
+
call()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_http_error_with_wrong_status_code():
|
|
62
|
+
safe_params = RequestSafeMode(2, (HTTPStatus.NOT_FOUND,)) # Wrong Status
|
|
63
|
+
|
|
64
|
+
def call():
|
|
65
|
+
handle_response(mock_response(HTTPStatus.BAD_REQUEST), safe_params)
|
|
66
|
+
|
|
67
|
+
with pytest.raises(HTTPError):
|
|
68
|
+
call()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_http_error_with_return():
|
|
72
|
+
safe_params = RequestSafeMode(2, (HTTPStatus.NOT_FOUND,)) # Wrong Status
|
|
73
|
+
|
|
74
|
+
def call():
|
|
75
|
+
return handle_response(mock_response(HTTPStatus.OK), safe_params)
|
|
76
|
+
|
|
77
|
+
assert call() == [{"data": "working"}]
|
|
@@ -1,13 +1,2 @@
|
|
|
1
1
|
DEFAULT_API_VERSION = 59.0
|
|
2
2
|
DEFAULT_PAGINATION_LIMIT = 100
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Keys:
|
|
6
|
-
"""Salesforce's credentials keys"""
|
|
7
|
-
|
|
8
|
-
USERNAME = "username"
|
|
9
|
-
PASSWORD = "password" # noqa: S105
|
|
10
|
-
CLIENT_ID = "client_id"
|
|
11
|
-
CLIENT_SECRET = "client_secret" # noqa: S105
|
|
12
|
-
SECURITY_TOKEN = "security_token" # noqa: S105
|
|
13
|
-
BASE_URL = "base_url"
|
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
from typing import Dict
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
_PASSWORD = "CASTOR_SALESFORCE_PASSWORD" # noqa: S105
|
|
8
|
-
_SECURITY_TOKEN = "CASTOR_SALESFORCE_SECURITY_TOKEN" # noqa: S105
|
|
9
|
-
_CLIENT_ID = "CASTOR_SALESFORCE_CLIENT_ID"
|
|
10
|
-
_CLIENT_SECRET = "CASTOR_SALESFORCE_CLIENT_SECRET" # noqa: S105
|
|
11
|
-
_BASE_URL = "CASTOR_SALESFORCE_BASE_URL"
|
|
6
|
+
CASTOR_ENV_PREFIX = "CASTOR_SALESFORCE_"
|
|
12
7
|
|
|
13
8
|
|
|
14
|
-
class SalesforceCredentials:
|
|
9
|
+
class SalesforceCredentials(BaseSettings):
|
|
15
10
|
"""
|
|
16
11
|
Class to handle Salesforce rest API permissions
|
|
17
12
|
"""
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
model_config = SettingsConfigDict(
|
|
15
|
+
env_prefix=CASTOR_ENV_PREFIX,
|
|
16
|
+
extra="ignore",
|
|
17
|
+
populate_by_name=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
base_url: str
|
|
21
|
+
client_id: str
|
|
22
|
+
client_secret: str = Field(repr=False)
|
|
23
|
+
password: str = Field(repr=False)
|
|
24
|
+
security_token: str = Field(repr=False)
|
|
25
|
+
username: str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def password_token(self) -> str:
|
|
29
|
+
"""Generates the password for authentication"""
|
|
30
|
+
return self.password + self.security_token
|
|
34
31
|
|
|
35
32
|
def token_request_payload(self) -> Dict[str, str]:
|
|
36
33
|
"""
|
|
@@ -41,25 +38,5 @@ class SalesforceCredentials:
|
|
|
41
38
|
"client_id": self.client_id,
|
|
42
39
|
"client_secret": self.client_secret,
|
|
43
40
|
"username": self.username,
|
|
44
|
-
"password": self.
|
|
41
|
+
"password": self.password_token,
|
|
45
42
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def to_credentials(params: dict) -> SalesforceCredentials:
|
|
49
|
-
"""extract Salesforce credentials"""
|
|
50
|
-
username = params.get(Keys.USERNAME) or from_env(_USERNAME)
|
|
51
|
-
password = params.get(Keys.PASSWORD) or from_env(_PASSWORD)
|
|
52
|
-
security_token = params.get(Keys.SECURITY_TOKEN) or from_env(
|
|
53
|
-
_SECURITY_TOKEN
|
|
54
|
-
)
|
|
55
|
-
client_id = params.get(Keys.CLIENT_ID) or from_env(_CLIENT_ID)
|
|
56
|
-
client_secret = params.get(Keys.CLIENT_SECRET) or from_env(_CLIENT_SECRET)
|
|
57
|
-
base_url = params.get(Keys.BASE_URL) or from_env(_BASE_URL)
|
|
58
|
-
return SalesforceCredentials(
|
|
59
|
-
username=username,
|
|
60
|
-
password=password,
|
|
61
|
-
client_id=client_id,
|
|
62
|
-
client_secret=client_secret,
|
|
63
|
-
security_token=security_token,
|
|
64
|
-
base_url=base_url,
|
|
65
|
-
)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .client import DomoClient
|
|
2
|
-
from .credentials import
|
|
2
|
+
from .credentials import DomoCredentials
|