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.

Files changed (92) hide show
  1. CHANGELOG.md +28 -0
  2. DockerfileUsage.md +21 -0
  3. castor_extractor/commands/extract_domo.py +2 -10
  4. castor_extractor/commands/extract_looker.py +2 -13
  5. castor_extractor/commands/extract_metabase_api.py +5 -10
  6. castor_extractor/commands/extract_metabase_db.py +6 -16
  7. castor_extractor/commands/extract_mode.py +2 -13
  8. castor_extractor/commands/extract_powerbi.py +2 -8
  9. castor_extractor/commands/extract_qlik.py +2 -7
  10. castor_extractor/commands/extract_salesforce.py +3 -12
  11. castor_extractor/commands/extract_salesforce_reporting.py +2 -10
  12. castor_extractor/commands/extract_sigma.py +2 -7
  13. castor_extractor/utils/__init__.py +3 -1
  14. castor_extractor/utils/argument_parser.py +7 -0
  15. castor_extractor/utils/argument_parser_test.py +25 -0
  16. castor_extractor/utils/collection.py +8 -0
  17. castor_extractor/utils/safe_request.py +57 -0
  18. castor_extractor/utils/safe_request_test.py +77 -0
  19. castor_extractor/utils/salesforce/__init__.py +1 -2
  20. castor_extractor/utils/salesforce/constants.py +0 -11
  21. castor_extractor/utils/salesforce/credentials.py +22 -45
  22. castor_extractor/visualization/domo/__init__.py +1 -1
  23. castor_extractor/visualization/domo/client/__init__.py +1 -1
  24. castor_extractor/visualization/domo/client/client.py +37 -52
  25. castor_extractor/visualization/domo/client/credentials.py +14 -27
  26. castor_extractor/visualization/domo/extract.py +5 -26
  27. castor_extractor/visualization/looker/__init__.py +6 -1
  28. castor_extractor/visualization/looker/api/__init__.py +2 -1
  29. castor_extractor/visualization/looker/api/client.py +6 -4
  30. castor_extractor/visualization/looker/api/client_test.py +5 -3
  31. castor_extractor/visualization/looker/api/credentials.py +33 -0
  32. castor_extractor/visualization/looker/api/extraction_parameters.py +38 -0
  33. castor_extractor/visualization/looker/api/sdk.py +2 -28
  34. castor_extractor/visualization/looker/constant.py +2 -27
  35. castor_extractor/visualization/looker/constants.py +17 -0
  36. castor_extractor/visualization/looker/extract.py +29 -29
  37. castor_extractor/visualization/metabase/__init__.py +6 -1
  38. castor_extractor/visualization/metabase/client/__init__.py +2 -2
  39. castor_extractor/visualization/metabase/client/api/__init__.py +1 -0
  40. castor_extractor/visualization/metabase/client/api/client.py +8 -14
  41. castor_extractor/visualization/metabase/client/api/credentials.py +13 -40
  42. castor_extractor/visualization/metabase/client/db/__init__.py +1 -0
  43. castor_extractor/visualization/metabase/client/db/client.py +13 -34
  44. castor_extractor/visualization/metabase/client/db/credentials.py +19 -73
  45. castor_extractor/visualization/metabase/errors.py +5 -3
  46. castor_extractor/visualization/metabase/extract.py +3 -3
  47. castor_extractor/visualization/mode/__init__.py +1 -1
  48. castor_extractor/visualization/mode/client/__init__.py +1 -0
  49. castor_extractor/visualization/mode/client/client.py +9 -12
  50. castor_extractor/visualization/mode/client/client_test.py +3 -3
  51. castor_extractor/visualization/mode/client/credentials.py +18 -51
  52. castor_extractor/visualization/mode/extract.py +6 -3
  53. castor_extractor/visualization/powerbi/__init__.py +1 -1
  54. castor_extractor/visualization/powerbi/client/__init__.py +2 -1
  55. castor_extractor/visualization/powerbi/client/credentials.py +17 -9
  56. castor_extractor/visualization/powerbi/client/credentials_test.py +12 -4
  57. castor_extractor/visualization/powerbi/client/rest.py +2 -2
  58. castor_extractor/visualization/powerbi/client/rest_test.py +2 -2
  59. castor_extractor/visualization/powerbi/extract.py +5 -16
  60. castor_extractor/visualization/qlik/__init__.py +5 -1
  61. castor_extractor/visualization/qlik/client/__init__.py +1 -0
  62. castor_extractor/visualization/qlik/client/engine/__init__.py +1 -0
  63. castor_extractor/visualization/qlik/client/engine/client.py +5 -6
  64. castor_extractor/visualization/qlik/client/engine/credentials.py +26 -0
  65. castor_extractor/visualization/qlik/client/master.py +5 -11
  66. castor_extractor/visualization/qlik/client/rest.py +4 -4
  67. castor_extractor/visualization/qlik/client/rest_test.py +6 -2
  68. castor_extractor/visualization/qlik/extract.py +6 -13
  69. castor_extractor/visualization/salesforce_reporting/extract.py +6 -20
  70. castor_extractor/visualization/sigma/__init__.py +1 -1
  71. castor_extractor/visualization/sigma/client/__init__.py +1 -1
  72. castor_extractor/visualization/sigma/client/client.py +5 -4
  73. castor_extractor/visualization/sigma/client/credentials.py +12 -28
  74. castor_extractor/visualization/sigma/extract.py +5 -18
  75. castor_extractor/visualization/tableau_revamp/client/credentials.py +40 -87
  76. castor_extractor/warehouse/databricks/client.py +3 -0
  77. castor_extractor/warehouse/redshift/queries/column.sql +0 -5
  78. castor_extractor/warehouse/salesforce/extract.py +2 -2
  79. castor_extractor/warehouse/salesforce/format.py +5 -3
  80. castor_extractor/warehouse/snowflake/queries/column.sql +0 -1
  81. castor_extractor/warehouse/synapse/queries/column.sql +0 -1
  82. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/METADATA +9 -9
  83. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/RECORD +86 -83
  84. castor_extractor/visualization/domo/client/client_test.py +0 -60
  85. castor_extractor/visualization/domo/constants.py +0 -6
  86. castor_extractor/visualization/looker/env.py +0 -48
  87. castor_extractor/visualization/looker/parameters.py +0 -78
  88. castor_extractor/visualization/qlik/constants.py +0 -3
  89. castor_extractor/visualization/sigma/constants.py +0 -4
  90. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/LICENCE +0 -0
  91. {castor_extractor-0.17.4.dist-info → castor_extractor-0.18.5.dist-info}/WHEEL +0 -0
  92. {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
- args = parser.parse_args()
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
- args = parser.parse_args()
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.parse_args()
19
+ args = parse_filled_arguments(parser)
19
20
 
20
- client = metabase.ApiClient(
21
- base_url=args.base_url,
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.parse_args()
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.extract_all(
50
- client,
51
- output_directory=args.output,
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
- args = parser.parse_args()
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
- args = parser.parse_args()
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
- args = parser.parse_args()
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.parse_args()
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
- args = parser.parse_args()
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
- args = parser.parse_args()
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,3 +1,2 @@
1
1
  from .client import SalesforceBaseClient
2
- from .constants import Keys
3
- from .credentials import SalesforceCredentials, to_credentials
2
+ from .credentials import SalesforceCredentials
@@ -1,13 +1,2 @@
1
1
  DEFAULT_API_VERSION = 59.0
2
2
  DEFAULT_PAGINATION_LIMIT = 100
3
-
4
-
5
- class Keys:
6
- """Salesforce's credentials keys"""
7
-
8
- USERNAME = "username"
9
- PASSWORD = "password" # noqa: S105
10
- CLIENT_ID = "client_id"
11
- CLIENT_SECRET = "client_secret" # noqa: S105
12
- SECURITY_TOKEN = "security_token" # noqa: S105
13
- BASE_URL = "base_url"
@@ -1,36 +1,33 @@
1
1
  from typing import Dict
2
2
 
3
- from ...utils import from_env
4
- from .constants import Keys
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
5
 
6
- _USERNAME = "CASTOR_SALESFORCE_USERNAME"
7
- _PASSWORD = "CASTOR_SALESFORCE_PASSWORD" # noqa: S105
8
- _SECURITY_TOKEN = "CASTOR_SALESFORCE_SECURITY_TOKEN" # noqa: S105
9
- _CLIENT_ID = "CASTOR_SALESFORCE_CLIENT_ID"
10
- _CLIENT_SECRET = "CASTOR_SALESFORCE_CLIENT_SECRET" # noqa: S105
11
- _BASE_URL = "CASTOR_SALESFORCE_BASE_URL"
6
+ CASTOR_ENV_PREFIX = "CASTOR_SALESFORCE_"
12
7
 
13
8
 
14
- class SalesforceCredentials:
9
+ class SalesforceCredentials(BaseSettings):
15
10
  """
16
11
  Class to handle Salesforce rest API permissions
17
12
  """
18
13
 
19
- def __init__(
20
- self,
21
- *,
22
- username: str,
23
- password: str,
24
- security_token: str,
25
- client_id: str,
26
- client_secret: str,
27
- base_url: str,
28
- ):
29
- self.username = username
30
- self.password = password + security_token
31
- self.client_id = client_id
32
- self.client_secret = client_secret
33
- self.base_url = base_url
14
+ model_config = SettingsConfigDict(
15
+ env_prefix=CASTOR_ENV_PREFIX,
16
+ extra="ignore",
17
+ populate_by_name=True,
18
+ )
19
+
20
+ base_url: str
21
+ client_id: str
22
+ client_secret: str = Field(repr=False)
23
+ password: str = Field(repr=False)
24
+ security_token: str = Field(repr=False)
25
+ username: str
26
+
27
+ @property
28
+ def password_token(self) -> str:
29
+ """Generates the password for authentication"""
30
+ return self.password + self.security_token
34
31
 
35
32
  def token_request_payload(self) -> Dict[str, str]:
36
33
  """
@@ -41,25 +38,5 @@ class SalesforceCredentials:
41
38
  "client_id": self.client_id,
42
39
  "client_secret": self.client_secret,
43
40
  "username": self.username,
44
- "password": self.password,
41
+ "password": self.password_token,
45
42
  }
46
-
47
-
48
- def to_credentials(params: dict) -> SalesforceCredentials:
49
- """extract Salesforce credentials"""
50
- username = params.get(Keys.USERNAME) or from_env(_USERNAME)
51
- password = params.get(Keys.PASSWORD) or from_env(_PASSWORD)
52
- security_token = params.get(Keys.SECURITY_TOKEN) or from_env(
53
- _SECURITY_TOKEN
54
- )
55
- client_id = params.get(Keys.CLIENT_ID) or from_env(_CLIENT_ID)
56
- client_secret = params.get(Keys.CLIENT_SECRET) or from_env(_CLIENT_SECRET)
57
- base_url = params.get(Keys.BASE_URL) or from_env(_BASE_URL)
58
- return SalesforceCredentials(
59
- username=username,
60
- password=password,
61
- client_id=client_id,
62
- client_secret=client_secret,
63
- security_token=security_token,
64
- base_url=base_url,
65
- )
@@ -1,3 +1,3 @@
1
1
  from .assets import DomoAsset
2
- from .client import CredentialsKey, DomoClient, DomoCredentials
2
+ from .client import DomoClient, DomoCredentials
3
3
  from .extract import extract_all
@@ -1,2 +1,2 @@
1
1
  from .client import DomoClient
2
- from .credentials import CredentialsKey, DomoCredentials
2
+ from .credentials import DomoCredentials