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/core/session.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import InitVar, dataclass, field
|
|
5
|
+
from importlib.metadata import version as get_version
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from pycarlo.common import get_logger
|
|
9
|
+
from pycarlo.common.errors import InvalidConfigFileError, InvalidSessionError
|
|
10
|
+
from pycarlo.common.settings import (
|
|
11
|
+
DEFAULT_CONFIG_PATH,
|
|
12
|
+
DEFAULT_MCD_API_ENDPOINT,
|
|
13
|
+
DEFAULT_MCD_API_ENDPOINT_CONFIG_KEY,
|
|
14
|
+
DEFAULT_MCD_API_ID_CONFIG_KEY,
|
|
15
|
+
DEFAULT_MCD_API_TOKEN_CONFIG_KEY,
|
|
16
|
+
DEFAULT_MCD_IGW_ENDPOINT,
|
|
17
|
+
DEFAULT_PACKAGE_NAME,
|
|
18
|
+
DEFAULT_PROFILE_NAME,
|
|
19
|
+
MCD_API_ENDPOINT,
|
|
20
|
+
MCD_DEFAULT_API_ID,
|
|
21
|
+
MCD_DEFAULT_API_TOKEN,
|
|
22
|
+
MCD_DEFAULT_PROFILE,
|
|
23
|
+
MCD_USER_ID_HEADER,
|
|
24
|
+
PROFILE_FILE_NAME,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Session:
|
|
32
|
+
"""
|
|
33
|
+
Creates an MC access session.
|
|
34
|
+
|
|
35
|
+
Auth resolution hierarchy -
|
|
36
|
+
1. Passing credentials (mcd_id & mcd_token)
|
|
37
|
+
2. Environment variables (MCD_DEFAULT_API_ID & MCD_DEFAULT_API_TOKEN)
|
|
38
|
+
3. Config-file by passing passing profile name (mcd_profile)
|
|
39
|
+
4. Config-file by setting the profile as an environment variable (MCD_DEFAULT_PROFILE)
|
|
40
|
+
5. Config-file by default profile name (default)
|
|
41
|
+
|
|
42
|
+
Environment vars can be mixed with passed credentials, but not the config-file profile.
|
|
43
|
+
|
|
44
|
+
If necessary the MC API url can be overridden by specifying an endpoint.
|
|
45
|
+
|
|
46
|
+
The config-file path can be set via mcd_config_path.
|
|
47
|
+
|
|
48
|
+
An optional scope can be set to configure the Session to use the Integration Gateway
|
|
49
|
+
REST API instead of the GraphQL API.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
mcd_id: InitVar[Optional[str]] = None
|
|
53
|
+
mcd_token: InitVar[Optional[str]] = None
|
|
54
|
+
mcd_profile: InitVar[Optional[str]] = None
|
|
55
|
+
mcd_config_path: InitVar[str] = DEFAULT_CONFIG_PATH
|
|
56
|
+
|
|
57
|
+
id: str = field(init=False)
|
|
58
|
+
token: str = field(init=False)
|
|
59
|
+
session_name: str = field(init=False)
|
|
60
|
+
endpoint: str = DEFAULT_MCD_API_ENDPOINT
|
|
61
|
+
user_id: Optional[str] = MCD_USER_ID_HEADER
|
|
62
|
+
scope: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
def __post_init__(
|
|
65
|
+
self,
|
|
66
|
+
mcd_id: Optional[str],
|
|
67
|
+
mcd_token: Optional[str],
|
|
68
|
+
mcd_profile: Optional[str],
|
|
69
|
+
mcd_config_path: str,
|
|
70
|
+
):
|
|
71
|
+
version = get_version(DEFAULT_PACKAGE_NAME)
|
|
72
|
+
self.session_name = f"python-sdk-{version}-{uuid.uuid4()}"
|
|
73
|
+
logger.info(f"Creating named session as '{self.session_name}'.")
|
|
74
|
+
|
|
75
|
+
mcd_id = mcd_id or MCD_DEFAULT_API_ID
|
|
76
|
+
mcd_token = mcd_token or MCD_DEFAULT_API_TOKEN
|
|
77
|
+
if mcd_id and mcd_token:
|
|
78
|
+
self.id = mcd_id
|
|
79
|
+
self.token = mcd_token
|
|
80
|
+
elif mcd_id or mcd_token:
|
|
81
|
+
raise InvalidSessionError("Partially setting a session is not supported.")
|
|
82
|
+
else:
|
|
83
|
+
self._read_config(
|
|
84
|
+
mcd_profile=mcd_profile or MCD_DEFAULT_PROFILE or DEFAULT_PROFILE_NAME,
|
|
85
|
+
mcd_config_path=mcd_config_path,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if MCD_API_ENDPOINT:
|
|
89
|
+
self.endpoint = MCD_API_ENDPOINT
|
|
90
|
+
elif self.scope and self.endpoint == DEFAULT_MCD_API_ENDPOINT:
|
|
91
|
+
# if scope is set and endpoint is the default one, change it to IGW
|
|
92
|
+
self.endpoint = DEFAULT_MCD_IGW_ENDPOINT
|
|
93
|
+
|
|
94
|
+
session_type = "GATEWAY_API" if self.scope else "APPLICATION_API"
|
|
95
|
+
logger.info(f"Created {session_type} session with MC API ID '{self.id}'.")
|
|
96
|
+
|
|
97
|
+
def _read_config(self, mcd_profile: str, mcd_config_path: str) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Return configuration from section (profile name) if it exists.
|
|
100
|
+
"""
|
|
101
|
+
config_parser = Session._get_config_parser()
|
|
102
|
+
file_path = os.path.join(mcd_config_path, PROFILE_FILE_NAME)
|
|
103
|
+
logger.info(
|
|
104
|
+
"No provided connection details. Looking up session values from "
|
|
105
|
+
f"'{mcd_profile}' in '{file_path}'."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
config_parser.read(file_path)
|
|
110
|
+
self.id = config_parser.get(mcd_profile, DEFAULT_MCD_API_ID_CONFIG_KEY)
|
|
111
|
+
self.token = config_parser.get(mcd_profile, DEFAULT_MCD_API_TOKEN_CONFIG_KEY)
|
|
112
|
+
self.endpoint = config_parser.get(
|
|
113
|
+
mcd_profile,
|
|
114
|
+
DEFAULT_MCD_API_ENDPOINT_CONFIG_KEY,
|
|
115
|
+
fallback=DEFAULT_MCD_API_ENDPOINT,
|
|
116
|
+
)
|
|
117
|
+
except configparser.NoSectionError:
|
|
118
|
+
raise InvalidSessionError(f"Profile '{mcd_profile}' not found in '{file_path}'.")
|
|
119
|
+
except Exception as err:
|
|
120
|
+
raise InvalidConfigFileError from err
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _get_config_parser() -> configparser.ConfigParser:
|
|
124
|
+
"""
|
|
125
|
+
Gets a configparser
|
|
126
|
+
"""
|
|
127
|
+
return configparser.ConfigParser()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CircuitBreakerPipelineException(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CircuitBreakerPollException(Exception):
|
|
9
|
+
def __init__(self, msg: str = "Polling timed out or contains a malformed log.", *args: Any):
|
|
10
|
+
super().__init__(msg, *args)
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Callable, Dict, List, Optional, Sequence, Union, cast
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from box import Box
|
|
7
|
+
|
|
8
|
+
from pycarlo.common import get_logger
|
|
9
|
+
from pycarlo.common.settings import (
|
|
10
|
+
HEADER_MCD_TELEMETRY_REASON,
|
|
11
|
+
HEADER_MCD_TELEMETRY_SERVICE,
|
|
12
|
+
RequestReason,
|
|
13
|
+
)
|
|
14
|
+
from pycarlo.common.utils import boxify
|
|
15
|
+
from pycarlo.core import Client, Mutation, Query
|
|
16
|
+
from pycarlo.features.circuit_breakers.exceptions import (
|
|
17
|
+
CircuitBreakerPipelineException,
|
|
18
|
+
CircuitBreakerPollException,
|
|
19
|
+
)
|
|
20
|
+
from pycarlo.lib.schema import CircuitBreakerState, SqlJobCheckpointStatus
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CircuitBreakerService:
|
|
26
|
+
_TERM_STATES = {"PROCESSING_COMPLETE", "HAS_ERROR"}
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
mc_client: Optional[Client] = None,
|
|
31
|
+
print_func: Callable = logger.info,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Convenience methods to help with using circuit breaker rules.
|
|
35
|
+
|
|
36
|
+
:param mc_client: MCD client (e.g. for creating a custom session); created otherwise.
|
|
37
|
+
:param print_func: Function to use for echoing. Uses python logging by default, which
|
|
38
|
+
requires setting MCD_VERBOSE_ERRORS.
|
|
39
|
+
"""
|
|
40
|
+
self._client = mc_client or Client()
|
|
41
|
+
self._print_func = print_func
|
|
42
|
+
|
|
43
|
+
def trigger_and_poll(
|
|
44
|
+
self,
|
|
45
|
+
rule_uuid: Optional[Union[str, UUID]] = None,
|
|
46
|
+
namespace: Optional[str] = None,
|
|
47
|
+
rule_name: Optional[str] = None,
|
|
48
|
+
timeout_in_minutes: int = 5,
|
|
49
|
+
runtime_variables: Optional[Dict[str, str]] = None,
|
|
50
|
+
) -> Optional[bool]:
|
|
51
|
+
"""
|
|
52
|
+
Convenience method to both trigger and poll (wait) on circuit breaker rule execution.
|
|
53
|
+
|
|
54
|
+
:param rule_uuid: UUID of the rule (custom SQL monitor) to execute.
|
|
55
|
+
:param namespace: namespace of the rule (custom SQL monitor) to execute.
|
|
56
|
+
:param rule_name: name of the rule (custom SQL monitor) to execute.
|
|
57
|
+
:param timeout_in_minutes: Polling timeout in minutes. See poll() for details.
|
|
58
|
+
:param runtime_variables: runtime variables to use when executing the rule
|
|
59
|
+
:return: True if rule execution has breach; False otherwise. See poll() for any
|
|
60
|
+
exceptions raised.
|
|
61
|
+
"""
|
|
62
|
+
breaches = self.poll_all(
|
|
63
|
+
job_execution_uuids=self.trigger_all(
|
|
64
|
+
rule_uuid=rule_uuid,
|
|
65
|
+
namespace=namespace,
|
|
66
|
+
rule_name=rule_name,
|
|
67
|
+
runtime_variables=runtime_variables,
|
|
68
|
+
),
|
|
69
|
+
timeout_in_minutes=timeout_in_minutes,
|
|
70
|
+
)
|
|
71
|
+
return bool(breaches > 0)
|
|
72
|
+
|
|
73
|
+
def trigger(
|
|
74
|
+
self,
|
|
75
|
+
rule_uuid: Optional[Union[str, UUID]] = None,
|
|
76
|
+
namespace: Optional[str] = None,
|
|
77
|
+
rule_name: Optional[str] = None,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Trigger a rule to start execution with circuit breaker checkpointing.
|
|
81
|
+
|
|
82
|
+
:param rule_uuid: UUID of the rule (custom SQL monitor) to execute.
|
|
83
|
+
:param namespace: namespace of the rule (custom SQL monitor) to execute.
|
|
84
|
+
:param rule_name: name of the rule (custom SQL monitor) to execute.
|
|
85
|
+
:return: Job execution UUID, as a string, to be used to retrieve execution state / status.
|
|
86
|
+
"""
|
|
87
|
+
mutation = Mutation()
|
|
88
|
+
|
|
89
|
+
if rule_uuid:
|
|
90
|
+
mutation.trigger_circuit_breaker_rule(rule_uuid=str(rule_uuid)).__fields__(
|
|
91
|
+
"job_execution_uuid"
|
|
92
|
+
)
|
|
93
|
+
elif rule_name:
|
|
94
|
+
if namespace:
|
|
95
|
+
mutation.trigger_circuit_breaker_rule(
|
|
96
|
+
namespace=namespace, rule_name=rule_name
|
|
97
|
+
).__fields__("job_execution_uuid")
|
|
98
|
+
else:
|
|
99
|
+
mutation.trigger_circuit_breaker_rule(rule_name=rule_name).__fields__(
|
|
100
|
+
"job_execution_uuid"
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError("rule UUID or namespace and rule name must be specified")
|
|
104
|
+
|
|
105
|
+
mutation_client = self._client(
|
|
106
|
+
mutation,
|
|
107
|
+
additional_headers={
|
|
108
|
+
HEADER_MCD_TELEMETRY_REASON: RequestReason.SERVICE.value,
|
|
109
|
+
HEADER_MCD_TELEMETRY_SERVICE: "circuit_breaker_service",
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
job_execution_uuid = mutation_client.trigger_circuit_breaker_rule.job_execution_uuid
|
|
113
|
+
self._print_func(
|
|
114
|
+
f"Triggered rule with ID '{rule_uuid}'. "
|
|
115
|
+
f"Received '{job_execution_uuid}' as execution ID."
|
|
116
|
+
)
|
|
117
|
+
return cast(str, job_execution_uuid)
|
|
118
|
+
|
|
119
|
+
def trigger_all(
|
|
120
|
+
self,
|
|
121
|
+
rule_uuid: Optional[Union[str, UUID]] = None,
|
|
122
|
+
namespace: Optional[str] = None,
|
|
123
|
+
rule_name: Optional[str] = None,
|
|
124
|
+
runtime_variables: Optional[Dict[str, str]] = None,
|
|
125
|
+
) -> List[str]:
|
|
126
|
+
"""
|
|
127
|
+
Trigger a rule to start execution with circuit breaker checkpointing.
|
|
128
|
+
|
|
129
|
+
This function supports rules that create multiple executions (e.g. rules with variables
|
|
130
|
+
or over multiple tables)
|
|
131
|
+
|
|
132
|
+
:param rule_uuid: UUID of the rule (custom SQL monitor) to execute.
|
|
133
|
+
:param namespace: namespace of the rule (custom SQL monitor) to execute.
|
|
134
|
+
:param rule_name: name of the rule (custom SQL monitor) to execute.
|
|
135
|
+
:param runtime_variables: runtime variables to use when executing the rule
|
|
136
|
+
:return: Job execution UUIDs, as strings, to be used to retrieve execution state / status.
|
|
137
|
+
"""
|
|
138
|
+
mutation = Mutation()
|
|
139
|
+
|
|
140
|
+
runtime_variables_list: Optional[List[Dict[str, str]]]
|
|
141
|
+
if runtime_variables:
|
|
142
|
+
runtime_variables_list = [
|
|
143
|
+
{"name": key, "value": value} for key, value in runtime_variables.items()
|
|
144
|
+
]
|
|
145
|
+
else:
|
|
146
|
+
runtime_variables_list = None
|
|
147
|
+
|
|
148
|
+
if rule_uuid:
|
|
149
|
+
mutation.trigger_circuit_breaker_rule_v2(
|
|
150
|
+
rule_uuid=str(rule_uuid),
|
|
151
|
+
**({"runtime_variables": runtime_variables_list} if runtime_variables else {}),
|
|
152
|
+
).__fields__("job_execution_uuids")
|
|
153
|
+
elif rule_name:
|
|
154
|
+
if namespace:
|
|
155
|
+
mutation.trigger_circuit_breaker_rule_v2(
|
|
156
|
+
namespace=namespace,
|
|
157
|
+
rule_name=rule_name,
|
|
158
|
+
**({"runtime_variables": runtime_variables_list} if runtime_variables else {}),
|
|
159
|
+
).__fields__("job_execution_uuids")
|
|
160
|
+
else:
|
|
161
|
+
mutation.trigger_circuit_breaker_rule_v2(
|
|
162
|
+
rule_name=rule_name,
|
|
163
|
+
**({"runtime_variables": runtime_variables_list} if runtime_variables else {}),
|
|
164
|
+
).__fields__("job_execution_uuids")
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError("rule UUID or namespace and rule name must be specified")
|
|
167
|
+
|
|
168
|
+
job_execution_uuids = [
|
|
169
|
+
str(id)
|
|
170
|
+
for id in self._client(
|
|
171
|
+
mutation,
|
|
172
|
+
additional_headers={
|
|
173
|
+
HEADER_MCD_TELEMETRY_REASON: RequestReason.SERVICE.value,
|
|
174
|
+
HEADER_MCD_TELEMETRY_SERVICE: "circuit_breaker_service",
|
|
175
|
+
},
|
|
176
|
+
).trigger_circuit_breaker_rule_v2.job_execution_uuids
|
|
177
|
+
]
|
|
178
|
+
self._print_func(
|
|
179
|
+
f"Triggered rule with ID '{rule_uuid}'. "
|
|
180
|
+
f"Received {job_execution_uuids} as execution IDs."
|
|
181
|
+
)
|
|
182
|
+
return job_execution_uuids
|
|
183
|
+
|
|
184
|
+
def poll(
|
|
185
|
+
self,
|
|
186
|
+
job_execution_uuid: Union[str, UUID],
|
|
187
|
+
timeout_in_minutes: int = 5,
|
|
188
|
+
) -> Optional[int]:
|
|
189
|
+
"""
|
|
190
|
+
Poll status / state of an execution for a triggered rule. Polls until status is in a term
|
|
191
|
+
state or timeout.
|
|
192
|
+
|
|
193
|
+
:param job_execution_uuid: UUID for the job execution of a rule (custom SQL monitor).
|
|
194
|
+
:param timeout_in_minutes: Polling timeout in minutes. Note that The Data Collector Lambda
|
|
195
|
+
has a max timeout of 15 minutes when executing a query. Queries
|
|
196
|
+
that take longer to execute are not supported, so we recommend
|
|
197
|
+
filtering down the query output to improve performance (e.g limit
|
|
198
|
+
WHERE clause). If you expect a query to take the full 15 minutes
|
|
199
|
+
we recommend padding the timeout to 20 minutes.
|
|
200
|
+
:return: Breach count across all executions. A greater than 0 value indicates a breach.
|
|
201
|
+
:raise CircuitBreakerPipelineException: An error in executing the
|
|
202
|
+
rule (e.g. error in query).
|
|
203
|
+
:raise CircuitBreakerPollException: A timeout during polling or a malformed response.
|
|
204
|
+
"""
|
|
205
|
+
return self.poll_all([job_execution_uuid], timeout_in_minutes=timeout_in_minutes)
|
|
206
|
+
|
|
207
|
+
def poll_all(
|
|
208
|
+
self,
|
|
209
|
+
job_execution_uuids: Sequence[Union[str, UUID]],
|
|
210
|
+
timeout_in_minutes: int = 5,
|
|
211
|
+
) -> int:
|
|
212
|
+
"""
|
|
213
|
+
Poll status / state of executions for a triggered rule. Polls until status is in a term
|
|
214
|
+
state or timeout.
|
|
215
|
+
|
|
216
|
+
:param job_execution_uuids: UUIDs for the job executions of a rule (custom SQL monitor).
|
|
217
|
+
:param timeout_in_minutes: Polling timeout in minutes. Note that The Data Collector Lambda
|
|
218
|
+
has a max timeout of 15 minutes when executing a query. Queries
|
|
219
|
+
that take longer to execute are not supported, so we recommend
|
|
220
|
+
filtering down the query output to improve performance (e.g limit
|
|
221
|
+
WHERE clause). If you expect a query to take the full 15 minutes
|
|
222
|
+
we recommend padding the timeout to 20 minutes.
|
|
223
|
+
:return: Breach count. A greater than 0 value indicates a breach.
|
|
224
|
+
:raise CircuitBreakerPipelineException: An error in executing the
|
|
225
|
+
rule (e.g. error in query).
|
|
226
|
+
:raise CircuitBreakerPollException: A timeout during polling or a malformed response.
|
|
227
|
+
"""
|
|
228
|
+
logs = cast(
|
|
229
|
+
List[Box],
|
|
230
|
+
self._poll(
|
|
231
|
+
job_execution_uuids=job_execution_uuids,
|
|
232
|
+
timeout_in_minutes=timeout_in_minutes,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if not logs:
|
|
237
|
+
raise CircuitBreakerPollException
|
|
238
|
+
|
|
239
|
+
self._print_func(
|
|
240
|
+
"Completed polling. Retrieved execution with logs "
|
|
241
|
+
f"{list(map(str, logs))} for IDs {job_execution_uuids}."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
breaches = 0
|
|
245
|
+
has_breaches = False
|
|
246
|
+
if logs and len(logs) > 0:
|
|
247
|
+
for log in logs:
|
|
248
|
+
if log.payload.error:
|
|
249
|
+
logs_str = "\n".join(str(log) for log in logs)
|
|
250
|
+
raise CircuitBreakerPipelineException(
|
|
251
|
+
f"Execution pipeline errored out. Details:\n{logs_str}"
|
|
252
|
+
)
|
|
253
|
+
if log.payload.breach_count is not None:
|
|
254
|
+
breaches += log.payload.breach_count
|
|
255
|
+
has_breaches = True
|
|
256
|
+
|
|
257
|
+
if not has_breaches:
|
|
258
|
+
raise CircuitBreakerPollException
|
|
259
|
+
|
|
260
|
+
return breaches
|
|
261
|
+
|
|
262
|
+
@boxify(use_snakes=True, default_box_attr=None, default_box=True)
|
|
263
|
+
def _poll(
|
|
264
|
+
self,
|
|
265
|
+
job_execution_uuids: Sequence[Union[str, UUID]],
|
|
266
|
+
timeout_in_minutes: int,
|
|
267
|
+
sleep_interval_in_seconds: int = 15,
|
|
268
|
+
) -> Optional[List[Box]]:
|
|
269
|
+
timeout_start = time.time()
|
|
270
|
+
while time.time() < timeout_start + 60 * timeout_in_minutes:
|
|
271
|
+
query = Query()
|
|
272
|
+
query.get_circuit_breaker_rule_state_v2(
|
|
273
|
+
job_execution_uuids=map(str, job_execution_uuids)
|
|
274
|
+
).__fields__("status", "log")
|
|
275
|
+
circuit_rule_breaker_states = cast(
|
|
276
|
+
List[CircuitBreakerState],
|
|
277
|
+
self._client(
|
|
278
|
+
query,
|
|
279
|
+
additional_headers={
|
|
280
|
+
HEADER_MCD_TELEMETRY_REASON: RequestReason.SERVICE.value,
|
|
281
|
+
HEADER_MCD_TELEMETRY_SERVICE: "circuit_breaker_service",
|
|
282
|
+
},
|
|
283
|
+
).get_circuit_breaker_rule_state_v2,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
aggregated_status = self._get_aggregated_status(circuit_rule_breaker_states)
|
|
287
|
+
self._print_func(
|
|
288
|
+
f"Retrieved execution with aggregated status '{aggregated_status}' for "
|
|
289
|
+
f"IDs {job_execution_uuids}."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if aggregated_status in self._TERM_STATES:
|
|
293
|
+
return self._get_payloads(circuit_rule_breaker_states, aggregated_status)
|
|
294
|
+
|
|
295
|
+
self._print_func(
|
|
296
|
+
f"Aggregated state is not terminal state for IDs {job_execution_uuids}. "
|
|
297
|
+
f"Polling again in '{sleep_interval_in_seconds}' seconds."
|
|
298
|
+
)
|
|
299
|
+
time.sleep(sleep_interval_in_seconds)
|
|
300
|
+
|
|
301
|
+
def _get_log_payload(self, log: str):
|
|
302
|
+
log_entries = json.loads(log)
|
|
303
|
+
log_entries.reverse()
|
|
304
|
+
for entry in log_entries:
|
|
305
|
+
if "payload" in entry:
|
|
306
|
+
return Box(entry, default_box_attr=None, default_box=True)
|
|
307
|
+
return Box()
|
|
308
|
+
|
|
309
|
+
def _get_payloads(
|
|
310
|
+
self,
|
|
311
|
+
states: List[CircuitBreakerState],
|
|
312
|
+
status: SqlJobCheckpointStatus,
|
|
313
|
+
) -> List[Box]:
|
|
314
|
+
payloads = []
|
|
315
|
+
for state in states:
|
|
316
|
+
if state.status == status:
|
|
317
|
+
payloads.append(self._get_log_payload(str(state.log)))
|
|
318
|
+
return payloads
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _get_aggregated_status(states: List[CircuitBreakerState]) -> SqlJobCheckpointStatus:
|
|
322
|
+
if not states:
|
|
323
|
+
return SqlJobCheckpointStatus.REGISTERED # type: ignore
|
|
324
|
+
|
|
325
|
+
status_by_state = {}
|
|
326
|
+
for state in states:
|
|
327
|
+
status_by_state.setdefault(state.status, []).append(state)
|
|
328
|
+
|
|
329
|
+
def all_in_state(s: SqlJobCheckpointStatus):
|
|
330
|
+
return len(status_by_state.get(s, [])) == len(states)
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
SqlJobCheckpointStatus.HAS_ERROR # type: ignore
|
|
334
|
+
if status_by_state.get(SqlJobCheckpointStatus.HAS_ERROR) # type: ignore
|
|
335
|
+
else SqlJobCheckpointStatus.PROCESSING_COMPLETE # type: ignore
|
|
336
|
+
if all_in_state(SqlJobCheckpointStatus.PROCESSING_COMPLETE) # type: ignore
|
|
337
|
+
else SqlJobCheckpointStatus.PROCESSING_START # type: ignore
|
|
338
|
+
if status_by_state.get(SqlJobCheckpointStatus.PROCESSING_COMPLETE) # type: ignore
|
|
339
|
+
or all_in_state(SqlJobCheckpointStatus.PROCESSING_START) # type: ignore
|
|
340
|
+
else SqlJobCheckpointStatus.EXECUTING_COMPLETE # type: ignore
|
|
341
|
+
if all_in_state(SqlJobCheckpointStatus.PROCESSING_COMPLETE) # type: ignore
|
|
342
|
+
else SqlJobCheckpointStatus.EXECUTING_START # type: ignore
|
|
343
|
+
if status_by_state.get(SqlJobCheckpointStatus.EXECUTING_COMPLETE) # type: ignore
|
|
344
|
+
or all_in_state(SqlJobCheckpointStatus.EXECUTING_START) # type: ignore
|
|
345
|
+
else SqlJobCheckpointStatus.REGISTERED # type: ignore
|
|
346
|
+
)
|