pycarlo 0.12.24__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 pycarlo might be problematic. Click here for more details.
- pycarlo/__init__.py +0 -0
- pycarlo/common/__init__.py +31 -0
- pycarlo/common/errors.py +31 -0
- pycarlo/common/files.py +78 -0
- pycarlo/common/http.py +36 -0
- pycarlo/common/mcon.py +26 -0
- pycarlo/common/retries.py +129 -0
- pycarlo/common/settings.py +89 -0
- pycarlo/common/utils.py +51 -0
- pycarlo/core/__init__.py +10 -0
- pycarlo/core/client.py +267 -0
- pycarlo/core/endpoint.py +289 -0
- pycarlo/core/operations.py +25 -0
- pycarlo/core/session.py +127 -0
- pycarlo/features/__init__.py +10 -0
- pycarlo/features/circuit_breakers/__init__.py +3 -0
- pycarlo/features/circuit_breakers/exceptions.py +10 -0
- pycarlo/features/circuit_breakers/service.py +346 -0
- pycarlo/features/dbt/__init__.py +3 -0
- pycarlo/features/dbt/dbt_importer.py +208 -0
- pycarlo/features/dbt/queries.py +31 -0
- pycarlo/features/exceptions.py +18 -0
- pycarlo/features/metadata/__init__.py +32 -0
- pycarlo/features/metadata/asset_allow_block_list.py +22 -0
- pycarlo/features/metadata/asset_filters_container.py +79 -0
- pycarlo/features/metadata/base_allow_block_list.py +137 -0
- pycarlo/features/metadata/metadata_allow_block_list.py +94 -0
- pycarlo/features/metadata/metadata_filters_container.py +262 -0
- pycarlo/features/pii/__init__.py +5 -0
- pycarlo/features/pii/constants.py +3 -0
- pycarlo/features/pii/pii_filterer.py +179 -0
- pycarlo/features/pii/queries.py +20 -0
- pycarlo/features/pii/service.py +56 -0
- pycarlo/features/user/__init__.py +4 -0
- pycarlo/features/user/exceptions.py +10 -0
- pycarlo/features/user/models.py +9 -0
- pycarlo/features/user/queries.py +13 -0
- pycarlo/features/user/service.py +71 -0
- pycarlo/lib/README.md +35 -0
- pycarlo/lib/__init__.py +0 -0
- pycarlo/lib/schema.json +210020 -0
- pycarlo/lib/schema.py +82620 -0
- pycarlo/lib/types.py +68 -0
- pycarlo-0.12.24.dist-info/LICENSE +201 -0
- pycarlo-0.12.24.dist-info/METADATA +249 -0
- pycarlo-0.12.24.dist-info/RECORD +48 -0
- pycarlo-0.12.24.dist-info/WHEEL +5 -0
- pycarlo-0.12.24.dist-info/top_level.txt +1 -0
pycarlo/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from pycarlo.common.files import BytesFileReader, JsonFileReader, TextFileReader
|
|
4
|
+
from pycarlo.common.mcon import MCONParser, ParsedMCON
|
|
5
|
+
from pycarlo.common.settings import MCD_VERBOSE_ERRORS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_logger(name: str) -> logging.Logger:
|
|
9
|
+
"""
|
|
10
|
+
Returns a logger with the specified name.
|
|
11
|
+
|
|
12
|
+
:param name: Name of the logger.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(name)
|
|
16
|
+
handler = logging.StreamHandler()
|
|
17
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
18
|
+
handler.setFormatter(formatter)
|
|
19
|
+
logger.addHandler(handler)
|
|
20
|
+
logger.setLevel(logging.DEBUG if MCD_VERBOSE_ERRORS else logging.CRITICAL)
|
|
21
|
+
return logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BytesFileReader",
|
|
26
|
+
"JsonFileReader",
|
|
27
|
+
"MCONParser",
|
|
28
|
+
"ParsedMCON",
|
|
29
|
+
"TextFileReader",
|
|
30
|
+
"get_logger",
|
|
31
|
+
]
|
pycarlo/common/errors.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Dict, Mapping, Optional, Union
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GqlError(Exception):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
body: Optional[Union[Dict, str]] = None,
|
|
8
|
+
headers: Optional[Mapping] = None,
|
|
9
|
+
message: str = "",
|
|
10
|
+
status_code: int = 0,
|
|
11
|
+
summary: str = "",
|
|
12
|
+
retryable: Optional[bool] = None,
|
|
13
|
+
):
|
|
14
|
+
self.body = body
|
|
15
|
+
self.headers = headers
|
|
16
|
+
self.message = message
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.summary = summary
|
|
19
|
+
self.retryable = status_code >= 500 if retryable is None else retryable
|
|
20
|
+
super(GqlError, self).__init__()
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return self.summary
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidSessionError(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InvalidConfigFileError(Exception):
|
|
31
|
+
pass
|
pycarlo/common/files.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Callable, Dict, Generic, TypeVar, Union
|
|
4
|
+
|
|
5
|
+
# file reader return type
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileReader(Generic[T]):
|
|
10
|
+
"""
|
|
11
|
+
Utility for reading a local file. Return type is determined by the given `decoder` function.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, path: Union[Path, str], decoder: Callable[[bytes], T]):
|
|
15
|
+
"""
|
|
16
|
+
:param path: local file path
|
|
17
|
+
:param decoder: function that translates file content as bytes into an object of type T
|
|
18
|
+
"""
|
|
19
|
+
self._path = to_path(path)
|
|
20
|
+
self._decoder = decoder
|
|
21
|
+
|
|
22
|
+
def read(self) -> T:
|
|
23
|
+
"""
|
|
24
|
+
Read local file.
|
|
25
|
+
|
|
26
|
+
:return: contents of file, represented as type T
|
|
27
|
+
"""
|
|
28
|
+
with open(self._path, "rb") as file:
|
|
29
|
+
content = file.read()
|
|
30
|
+
return self._decoder(content)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BytesFileReader(FileReader[bytes]):
|
|
34
|
+
"""
|
|
35
|
+
Utility for reading a local file as bytes.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, path: Union[Path, str]):
|
|
39
|
+
"""
|
|
40
|
+
:param path: local file path
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(path=path, decoder=lambda b: b)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JsonFileReader(FileReader[Dict]):
|
|
46
|
+
"""
|
|
47
|
+
Utility for reading a local JSON file as a dictionary.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, path: Union[Path, str]):
|
|
51
|
+
"""
|
|
52
|
+
:param path: local file path
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(path=path, decoder=lambda b: json.loads(b))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TextFileReader(FileReader[str]):
|
|
58
|
+
"""
|
|
59
|
+
Utility for reading a local file as a string.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, path: Union[Path, str], encoding: str = "utf-8"):
|
|
63
|
+
"""
|
|
64
|
+
:param path: local file path
|
|
65
|
+
:param encoding: character encoding to use when translating file content to a string
|
|
66
|
+
"""
|
|
67
|
+
super().__init__(path=path, decoder=lambda b: b.decode(encoding))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def to_path(path: Union[Path, str]) -> Path:
|
|
71
|
+
"""
|
|
72
|
+
Simple function to normalize a file path passed as either a string or pathlib.Path to a
|
|
73
|
+
pathlib.Path instance.
|
|
74
|
+
|
|
75
|
+
:param path: local file path
|
|
76
|
+
:return: local file path represented as an instance of pathlib.Path
|
|
77
|
+
"""
|
|
78
|
+
return path if isinstance(path, Path) else Path(path)
|
pycarlo/common/http.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, Union
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from pycarlo.common.retries import ExponentialBackoffJitter, retry_with_backoff
|
|
7
|
+
from pycarlo.common.settings import DEFAULT_RETRY_INITIAL_WAIT_TIME, DEFAULT_RETRY_MAX_WAIT_TIME
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@retry_with_backoff(
|
|
11
|
+
backoff=ExponentialBackoffJitter(DEFAULT_RETRY_INITIAL_WAIT_TIME, DEFAULT_RETRY_MAX_WAIT_TIME),
|
|
12
|
+
exceptions=(requests.exceptions.ConnectionError, requests.exceptions.Timeout),
|
|
13
|
+
)
|
|
14
|
+
def upload(
|
|
15
|
+
url: str,
|
|
16
|
+
content: Union[bytes, str, Dict],
|
|
17
|
+
method: str = "post",
|
|
18
|
+
encoding: str = "utf-8",
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Upload a file to a given URL.
|
|
22
|
+
|
|
23
|
+
:param url: URL to use for upload
|
|
24
|
+
:param content: file content
|
|
25
|
+
:param method: HTTP method (e.g. 'post' or 'put')
|
|
26
|
+
:param encoding: character encoding to use (unless raw bytes are provided)
|
|
27
|
+
"""
|
|
28
|
+
if isinstance(content, str):
|
|
29
|
+
data = content.encode(encoding)
|
|
30
|
+
elif isinstance(content, dict):
|
|
31
|
+
data = json.dumps(content).encode(encoding)
|
|
32
|
+
else:
|
|
33
|
+
data = content
|
|
34
|
+
|
|
35
|
+
response = requests.request(method=method, url=url, data=data)
|
|
36
|
+
response.raise_for_status()
|
pycarlo/common/mcon.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ParsedMCON:
|
|
8
|
+
account_id: UUID
|
|
9
|
+
resource_id: UUID
|
|
10
|
+
object_type: str
|
|
11
|
+
object_id: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCONParser:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def parse_mcon(mcon: Optional[str]) -> Optional[ParsedMCON]:
|
|
17
|
+
if not mcon or not mcon.startswith("MCON++"):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
mcon_parts = mcon.split("++", 4)
|
|
21
|
+
return ParsedMCON(
|
|
22
|
+
account_id=UUID(mcon_parts[1]),
|
|
23
|
+
resource_id=UUID(mcon_parts[2]),
|
|
24
|
+
object_type=mcon_parts[3],
|
|
25
|
+
object_id=mcon_parts[4],
|
|
26
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import time
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, Generator, Optional, Tuple, Type, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Backoff(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Backoff is an abstract class that dictates a retry strategy.
|
|
11
|
+
It contains an abstract method `backoff` that returns a calculated delay based on the provided
|
|
12
|
+
attempt.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, start: float, maximum: float):
|
|
16
|
+
"""
|
|
17
|
+
Defines a new backoff retry strategy.
|
|
18
|
+
|
|
19
|
+
:param start: the scaling factor for any calculated delays. Must be >= 0.
|
|
20
|
+
:param maximum: defines a cap on the calculated delays to prevent prohibitively long waits
|
|
21
|
+
that could time out.
|
|
22
|
+
"""
|
|
23
|
+
if start < 0:
|
|
24
|
+
raise ValueError("start must be >= 0")
|
|
25
|
+
self.start = start
|
|
26
|
+
self.maximum = maximum
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def backoff(self, attempt: int) -> float:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def delays(self) -> Generator[float, None, None]:
|
|
33
|
+
"""
|
|
34
|
+
Generates a duration of time to delay for each successive call based on the configured
|
|
35
|
+
backoff strategy.
|
|
36
|
+
|
|
37
|
+
:return: a generator that yields the next delay duration.
|
|
38
|
+
"""
|
|
39
|
+
retries = 0
|
|
40
|
+
max_retries = 100 # Safety limit to prevent infinite loops
|
|
41
|
+
|
|
42
|
+
while retries < max_retries:
|
|
43
|
+
duration = self.backoff(retries)
|
|
44
|
+
# duration might be 0 when start == 0.
|
|
45
|
+
# In that case, retry once immediately, and then on maximum.
|
|
46
|
+
if duration >= self.maximum or duration <= 0:
|
|
47
|
+
break
|
|
48
|
+
yield duration
|
|
49
|
+
retries += 1
|
|
50
|
+
|
|
51
|
+
yield self.maximum
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def retry_with_backoff(
|
|
55
|
+
backoff: "Backoff",
|
|
56
|
+
exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]],
|
|
57
|
+
should_retry: Optional[Callable[[Exception], bool]] = None,
|
|
58
|
+
) -> Callable[[Callable], Callable]:
|
|
59
|
+
"""
|
|
60
|
+
A decorator to retry a function based on the :param:`backoff` provided
|
|
61
|
+
if any of the provided :param:`exceptions` are raised and the :param:`should_retry`
|
|
62
|
+
condition is met.
|
|
63
|
+
|
|
64
|
+
:param backoff: the retry strategy to employ.
|
|
65
|
+
:param exceptions: the exceptions that should trigger the retry. Can be further customized by
|
|
66
|
+
defining a custom attribute `retryable` on the exception class. The retries
|
|
67
|
+
are abandoned if retryable returns False.
|
|
68
|
+
:param should_retry: Optional callable to further determine whether to retry on an exception.
|
|
69
|
+
Takes an exception and returns a boolean. If `None`, all given exceptions
|
|
70
|
+
are retried.
|
|
71
|
+
:return: The same result the decorated function returns.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def _retry(func: Callable) -> Callable:
|
|
75
|
+
@wraps(func)
|
|
76
|
+
def _impl(*args: Any, **kwargs: Any) -> Any:
|
|
77
|
+
delays = backoff.delays()
|
|
78
|
+
while True:
|
|
79
|
+
try:
|
|
80
|
+
return func(*args, **kwargs)
|
|
81
|
+
except exceptions as exception:
|
|
82
|
+
# If the exception is marked as NOT retryable, stop now.
|
|
83
|
+
retryable = getattr(exception, "retryable", True)
|
|
84
|
+
if not retryable:
|
|
85
|
+
raise exception
|
|
86
|
+
# If a custom should_retry function is provided, call it to determine
|
|
87
|
+
# if we should retry or not. Otherwise, default to the retryable value.
|
|
88
|
+
current_should_retry = should_retry or (lambda _: retryable)
|
|
89
|
+
if not current_should_retry(exception):
|
|
90
|
+
raise exception
|
|
91
|
+
try:
|
|
92
|
+
delay = next(delays)
|
|
93
|
+
except StopIteration:
|
|
94
|
+
raise exception
|
|
95
|
+
time.sleep(delay)
|
|
96
|
+
|
|
97
|
+
return _impl
|
|
98
|
+
|
|
99
|
+
return _retry
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ExponentialBackoff(Backoff):
|
|
103
|
+
"""
|
|
104
|
+
A backoff strategy with an exponentially increasing delay in between attempts.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def exponential(self, attempt: int) -> float:
|
|
108
|
+
# Prevent overflow by using float arithmetic and limiting attempt size
|
|
109
|
+
if attempt > 1000: # Safety limit to prevent overflow
|
|
110
|
+
return self.maximum
|
|
111
|
+
return min(self.maximum, pow(2.0, float(attempt)) * self.start)
|
|
112
|
+
|
|
113
|
+
def backoff(self, attempt: int) -> float:
|
|
114
|
+
return self.exponential(attempt)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ExponentialBackoffJitter(ExponentialBackoff):
|
|
118
|
+
"""
|
|
119
|
+
An exponential backoff strategy with an added jitter that randomly spreads out the delays
|
|
120
|
+
uniformly while ensuring monotonically increasing delays.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def backoff(self, attempt: int) -> float:
|
|
124
|
+
base_delay = self.exponential(max(0, attempt - 1))
|
|
125
|
+
added_delay = self.exponential(max(0, attempt)) - base_delay
|
|
126
|
+
# Add jitter between 50% and 100% of the base delay to ensure monotonically increasing
|
|
127
|
+
# delays while still providing randomization
|
|
128
|
+
jitter_factor = random.uniform(0.5, 1.0)
|
|
129
|
+
return base_delay + (added_delay * jitter_factor)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
"""Environmental configuration"""
|
|
6
|
+
|
|
7
|
+
# Enable error logging.
|
|
8
|
+
MCD_VERBOSE_ERRORS = os.getenv("MCD_VERBOSE_ERRORS", False) in (True, "true", "True")
|
|
9
|
+
|
|
10
|
+
# MCD API endpoint.
|
|
11
|
+
MCD_API_ENDPOINT = os.getenv("MCD_API_ENDPOINT")
|
|
12
|
+
|
|
13
|
+
# Override MCD Default Profile when reading from the config-file in a session.
|
|
14
|
+
MCD_DEFAULT_PROFILE = os.getenv("MCD_DEFAULT_PROFILE")
|
|
15
|
+
|
|
16
|
+
# Override MCD API ID when creating a session.
|
|
17
|
+
MCD_DEFAULT_API_ID = os.getenv("MCD_DEFAULT_API_ID")
|
|
18
|
+
|
|
19
|
+
# Override MCD API Token when creating a session.
|
|
20
|
+
MCD_DEFAULT_API_TOKEN = os.getenv("MCD_DEFAULT_API_TOKEN")
|
|
21
|
+
|
|
22
|
+
# MCD ID header (for use in local development and testing)
|
|
23
|
+
MCD_USER_ID_HEADER = os.getenv("MCD_USER_ID_HEADER")
|
|
24
|
+
|
|
25
|
+
# dbt cloud API token
|
|
26
|
+
DBT_CLOUD_API_TOKEN = os.getenv("DBT_CLOUD_API_TOKEN")
|
|
27
|
+
|
|
28
|
+
# dbt cloud account ID
|
|
29
|
+
DBT_CLOUD_ACCOUNT_ID = os.getenv("DBT_CLOUD_ACCOUNT_ID")
|
|
30
|
+
|
|
31
|
+
"""Internal Use"""
|
|
32
|
+
|
|
33
|
+
# Default API endpoint when not provided through env variable nor profile
|
|
34
|
+
DEFAULT_MCD_API_ENDPOINT = "https://api.getmontecarlo.com/graphql"
|
|
35
|
+
|
|
36
|
+
# Default Gateway endpoint used when no endpoint is provided through env var or profile
|
|
37
|
+
DEFAULT_MCD_IGW_ENDPOINT = "https://integrations.getmontecarlo.com"
|
|
38
|
+
|
|
39
|
+
# Name of the current package.
|
|
40
|
+
DEFAULT_PACKAGE_NAME = "pycarlo"
|
|
41
|
+
|
|
42
|
+
# Default config keys for the MC config file. Created via the CLI.
|
|
43
|
+
DEFAULT_MCD_API_ID_CONFIG_KEY = "mcd_id"
|
|
44
|
+
DEFAULT_MCD_API_TOKEN_CONFIG_KEY = "mcd_token"
|
|
45
|
+
DEFAULT_MCD_API_ENDPOINT_CONFIG_KEY = "mcd_api_endpoint"
|
|
46
|
+
|
|
47
|
+
# Default headers for the MC API.
|
|
48
|
+
DEFAULT_MCD_API_ID_HEADER = f"x-{DEFAULT_MCD_API_ID_CONFIG_KEY.replace('_', '-')}"
|
|
49
|
+
DEFAULT_MCD_API_TOKEN_HEADER = f"x-{DEFAULT_MCD_API_TOKEN_CONFIG_KEY.replace('_', '-')}"
|
|
50
|
+
DEFAULT_MCD_USER_ID_HEADER = "user-id"
|
|
51
|
+
|
|
52
|
+
# Default headers to trace and help identify requests. For debugging.
|
|
53
|
+
DEFAULT_MCD_SESSION_ID = "x-mcd-session-id" # Generally the session name.
|
|
54
|
+
DEFAULT_MCD_TRACE_ID = "x-mcd-trace-id"
|
|
55
|
+
|
|
56
|
+
# File name for profile configuration.
|
|
57
|
+
PROFILE_FILE_NAME = "profiles.ini"
|
|
58
|
+
|
|
59
|
+
# Default profile to be used.
|
|
60
|
+
DEFAULT_PROFILE_NAME = "default"
|
|
61
|
+
|
|
62
|
+
# Default path where any configuration files are written.
|
|
63
|
+
DEFAULT_CONFIG_PATH = os.path.join(str(Path.home()), ".mcd")
|
|
64
|
+
|
|
65
|
+
# Default initial wait time for retries in seconds.
|
|
66
|
+
DEFAULT_RETRY_INITIAL_WAIT_TIME = 0.25
|
|
67
|
+
|
|
68
|
+
# Default maximum wait time for retries in seconds.
|
|
69
|
+
DEFAULT_RETRY_MAX_WAIT_TIME = 10.0
|
|
70
|
+
|
|
71
|
+
# Default initial wait time for idempotent request retries in seconds.
|
|
72
|
+
DEFAULT_IDEMPOTENT_RETRY_INITIAL_WAIT_TIME = 4.0
|
|
73
|
+
|
|
74
|
+
# Default maximum wait time for idempotent request retries in seconds.
|
|
75
|
+
DEFAULT_IDEMPOTENT_RETRY_MAX_WAIT_TIME = 4 * pow(
|
|
76
|
+
2, 4
|
|
77
|
+
) # retry 4 times, max wait 64 seconds, total wait 124
|
|
78
|
+
|
|
79
|
+
# Default timeout for requests sent to Integration Gateway
|
|
80
|
+
DEFAULT_IGW_TIMEOUT_SECS = 10
|
|
81
|
+
|
|
82
|
+
# Additional request headers
|
|
83
|
+
HEADER_MCD_TELEMETRY_REASON = "x-mcd-telemetry-reason" # why the request was made
|
|
84
|
+
HEADER_MCD_TELEMETRY_SERVICE = "x-mcd-telemetry-service" # what service made the request
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RequestReason(Enum):
|
|
88
|
+
USER = "user" # request made directly by a user
|
|
89
|
+
SERVICE = "service" # request made by a service, on behalf of a user or automation
|
pycarlo/common/utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any, Callable, List, Optional
|
|
4
|
+
|
|
5
|
+
from box import Box
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def chunks(lst: List, n: int):
|
|
9
|
+
"""Yield successive n-sized chunks from lst."""
|
|
10
|
+
for i in range(0, len(lst), n):
|
|
11
|
+
yield lst[i : i + n]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def boxify(
|
|
15
|
+
use_snakes: bool = False,
|
|
16
|
+
default_box_attr: Any = object(),
|
|
17
|
+
default_box: bool = False,
|
|
18
|
+
) -> Callable:
|
|
19
|
+
"""
|
|
20
|
+
Convenience decorator to convert a returned dictionary into a Box object for ease of use.
|
|
21
|
+
|
|
22
|
+
:param use_snakes: Convert camelCase into snake_case.
|
|
23
|
+
:param default_box_attr: Set default attribute to return.
|
|
24
|
+
:param default_box: Behave like a recursive default dict.
|
|
25
|
+
:return: Box object if original return was a Mapping (dictionary).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def _boxify(func: Callable):
|
|
29
|
+
@wraps(func)
|
|
30
|
+
def _impl(self: Any, *args: Any, **kwargs: Any) -> Optional[Box]:
|
|
31
|
+
dict_ = func(self, *args, **kwargs)
|
|
32
|
+
if dict_ and isinstance(dict_, Mapping):
|
|
33
|
+
return Box(
|
|
34
|
+
dict_,
|
|
35
|
+
camel_killer_box=use_snakes,
|
|
36
|
+
default_box_attr=default_box_attr,
|
|
37
|
+
default_box=default_box,
|
|
38
|
+
)
|
|
39
|
+
return dict_
|
|
40
|
+
|
|
41
|
+
return _impl
|
|
42
|
+
|
|
43
|
+
return _boxify
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def truncate_string(unicode_str: str, max_size: int) -> str:
|
|
47
|
+
str_bytes = unicode_str.encode("utf-8")
|
|
48
|
+
if len(str_bytes) < max_size:
|
|
49
|
+
return unicode_str # keep the original string
|
|
50
|
+
else:
|
|
51
|
+
return str_bytes[:max_size].decode("utf-8", errors="ignore")
|