recce-nightly 0.62.0.20250417__py3-none-any.whl → 1.30.0.20251221__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 recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +845 -461
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +59 -42
- recce/apis/check_events_api.py +353 -0
- recce/apis/check_func.py +41 -35
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +64 -25
- recce/artifact.py +119 -51
- recce/cli.py +1301 -324
- recce/config.py +43 -34
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404/index.html +2 -0
- recce/data/404.html +2 -1
- recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
- recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
- recce/data/__next.__PAGE__.txt +6 -0
- recce/data/__next._full.txt +32 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +14 -0
- recce/data/__next._tree.txt +8 -0
- recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
- recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
- recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
- recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
- recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
- recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
- recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
- recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
- recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
- recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
- recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
- recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
- recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
- recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
- recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
- recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
- recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
- recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
- recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
- recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
- recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
- recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
- recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
- recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
- recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
- recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
- recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
- recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
- recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
- recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
- recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
- recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
- recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
- recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
- recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
- recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
- recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
- recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
- recce/data/_not-found/__next._full.txt +24 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +13 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +6 -0
- recce/data/_not-found/index.html +2 -0
- recce/data/_not-found/index.txt +24 -0
- recce/data/auth_callback.html +68 -0
- recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/checks/__next._full.txt +39 -0
- recce/data/checks/__next._head.txt +8 -0
- recce/data/checks/__next._index.txt +14 -0
- recce/data/checks/__next._tree.txt +8 -0
- recce/data/checks/__next.checks.__PAGE__.txt +10 -0
- recce/data/checks/__next.checks.txt +4 -0
- recce/data/checks/index.html +2 -0
- recce/data/checks/index.txt +39 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +2 -27
- recce/data/index.txt +32 -7
- recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/lineage/__next._full.txt +39 -0
- recce/data/lineage/__next._head.txt +8 -0
- recce/data/lineage/__next._index.txt +14 -0
- recce/data/lineage/__next._tree.txt +8 -0
- recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
- recce/data/lineage/__next.lineage.txt +4 -0
- recce/data/lineage/index.html +2 -0
- recce/data/lineage/index.txt +39 -0
- recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/query/__next._full.txt +37 -0
- recce/data/query/__next._head.txt +8 -0
- recce/data/query/__next._index.txt +14 -0
- recce/data/query/__next._tree.txt +8 -0
- recce/data/query/__next.query.__PAGE__.txt +9 -0
- recce/data/query/__next.query.txt +4 -0
- recce/data/query/index.html +2 -0
- recce/data/query/index.txt +37 -0
- recce/diff.py +6 -12
- recce/event/CONFIG.bak +1 -0
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +725 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +438 -21
- recce/models/run.py +1 -0
- recce/models/types.py +134 -28
- recce/pull_request.py +27 -25
- recce/run.py +179 -122
- recce/server.py +394 -104
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +644 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +196 -149
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +180 -89
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/utils.py +147 -0
- recce/tasks/valuediff.py +247 -155
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +105 -100
- recce/util/cll.py +274 -219
- recce/util/cloud/__init__.py +15 -0
- recce/util/cloud/base.py +115 -0
- recce/util/cloud/check_events.py +190 -0
- recce/util/cloud/checks.py +242 -0
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +347 -72
- recce/util/singleton.py +4 -4
- recce/util/startup_perf.py +121 -0
- recce/yaml/__init__.py +7 -10
- recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
- recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
- recce/state.py +0 -753
- recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
- recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
- recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/adapter/__init__.py +0 -0
- tests/adapter/dbt_adapter/__init__.py +0 -0
- tests/adapter/dbt_adapter/conftest.py +0 -13
- tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
- tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
- tests/adapter/dbt_adapter/test_selector.py +0 -177
- tests/tasks/__init__.py +0 -0
- tests/tasks/conftest.py +0 -4
- tests/tasks/test_histogram.py +0 -137
- tests/tasks/test_lineage.py +0 -42
- tests/tasks/test_preset_checks.py +0 -50
- tests/tasks/test_profile.py +0 -73
- tests/tasks/test_query.py +0 -151
- tests/tasks/test_row_count.py +0 -116
- tests/tasks/test_schema.py +0 -99
- tests/tasks/test_top_k.py +0 -73
- tests/tasks/test_valuediff.py +0 -74
- tests/test_cli.py +0 -122
- tests/test_config.py +0 -45
- tests/test_core.py +0 -27
- tests/test_dbt.py +0 -36
- tests/test_pull_request.py +0 -130
- tests/test_server.py +0 -98
- tests/test_state.py +0 -123
- tests/test_summary.py +0 -57
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/util/cll.py
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Dict, List,
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from sqlglot
|
|
7
|
-
from sqlglot.
|
|
8
|
-
from sqlglot.optimizer import
|
|
5
|
+
import sqlglot.expressions as exp
|
|
6
|
+
from sqlglot import Dialect, parse_one
|
|
7
|
+
from sqlglot.errors import OptimizeError, SqlglotError
|
|
8
|
+
from sqlglot.optimizer import Scope, traverse_scope
|
|
9
9
|
from sqlglot.optimizer.qualify import qualify
|
|
10
10
|
|
|
11
11
|
from recce.exceptions import RecceException
|
|
12
|
-
from recce.
|
|
12
|
+
from recce.models.types import CllColumn, CllColumnDep
|
|
13
|
+
|
|
14
|
+
CllResult = Tuple[
|
|
15
|
+
List[CllColumnDep], # Model to column dependencies
|
|
16
|
+
Dict[str, CllColumn], # Column to column dependencies
|
|
17
|
+
]
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
@dataclass
|
|
16
|
-
class CLLPerformanceTracking
|
|
21
|
+
class CLLPerformanceTracking:
|
|
17
22
|
lineage_start = None
|
|
18
23
|
lineage_elapsed = None
|
|
19
24
|
column_lineage_start = None
|
|
@@ -50,11 +55,11 @@ class CLLPerformanceTracking(metaclass=SingletonMeta):
|
|
|
50
55
|
|
|
51
56
|
def to_dict(self):
|
|
52
57
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
"lineage_elapsed_ms": self.lineage_elapsed,
|
|
59
|
+
"column_lineage_elapsed_ms": self.column_lineage_elapsed,
|
|
60
|
+
"total_nodes": self.total_nodes,
|
|
61
|
+
"sqlglot_error_nodes": self.sqlglot_error_nodes,
|
|
62
|
+
"other_error_nodes": self.other_error_nodes,
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
def reset(self):
|
|
@@ -68,233 +73,283 @@ class CLLPerformanceTracking(metaclass=SingletonMeta):
|
|
|
68
73
|
self.other_error_nodes = 0
|
|
69
74
|
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
class ColumnLevelDependsOn:
|
|
73
|
-
node: str
|
|
74
|
-
column: str
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@dataclass
|
|
78
|
-
class ColumnLevelDependencyColumn:
|
|
79
|
-
type: Literal['source', 'passthrough', 'renamed', 'derived']
|
|
80
|
-
depends_on: List[ColumnLevelDependsOn]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _cll_expression(expression, table_alias_map) -> ColumnLevelDependencyColumn:
|
|
76
|
+
def _cll_column(proj, table_alias_map) -> CllColumn:
|
|
84
77
|
# given an expression, return the columns depends on
|
|
85
78
|
# [{node: table, column: column}, ...]
|
|
79
|
+
type = "source"
|
|
80
|
+
depends_on: List[CllColumnDep] = []
|
|
81
|
+
|
|
82
|
+
# instance of Column
|
|
83
|
+
if isinstance(proj, exp.Alias):
|
|
84
|
+
# 'select a as b'
|
|
85
|
+
# 'select CURRENT_TIMESTAMP() as create_at'
|
|
86
|
+
root = proj.this
|
|
87
|
+
|
|
88
|
+
for expression in root.walk(bfs=False):
|
|
89
|
+
if isinstance(expression, exp.Column):
|
|
90
|
+
column = expression
|
|
91
|
+
alias = column.table
|
|
92
|
+
|
|
93
|
+
if alias is None:
|
|
94
|
+
table = next(iter(table_alias_map.values()))
|
|
95
|
+
else:
|
|
96
|
+
table = table_alias_map.get(alias, alias)
|
|
97
|
+
depends_on.append(CllColumnDep(table, column.name))
|
|
98
|
+
if type == "source":
|
|
99
|
+
type = "passthrough"
|
|
100
|
+
elif isinstance(expression, (exp.Paren, exp.Identifier)):
|
|
101
|
+
pass
|
|
102
|
+
else:
|
|
103
|
+
type = "derived"
|
|
104
|
+
|
|
105
|
+
depends_on = _dedeup_depends_on(depends_on)
|
|
106
|
+
|
|
107
|
+
if len(depends_on) == 0:
|
|
108
|
+
type = "source"
|
|
109
|
+
|
|
110
|
+
if isinstance(proj, exp.Alias):
|
|
111
|
+
alias = proj
|
|
112
|
+
if type == "passthrough" and depends_on[0].column != alias.alias_or_name:
|
|
113
|
+
type = "renamed"
|
|
114
|
+
|
|
115
|
+
return CllColumn(type=type, depends_on=depends_on)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _dedeup_depends_on(depends_on: List[CllColumnDep]) -> List[CllColumnDep]:
|
|
119
|
+
# deduplicate the depends_on list
|
|
120
|
+
dedup_set = set()
|
|
121
|
+
dedup_list = []
|
|
122
|
+
for col_dep in depends_on:
|
|
123
|
+
node_col = col_dep.node + "." + col_dep.column
|
|
124
|
+
if node_col not in dedup_set:
|
|
125
|
+
dedup_list.append(col_dep)
|
|
126
|
+
dedup_set.add(node_col)
|
|
127
|
+
return dedup_list
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _cll_set_scope(scope: Scope, scope_cll_map: dict[Scope, CllResult]) -> CllResult:
|
|
131
|
+
# model-to-column
|
|
132
|
+
m2c: List[CllColumnDep] = []
|
|
133
|
+
# column-to-column
|
|
134
|
+
c2c_map: Dict[str, CllColumn] = {}
|
|
135
|
+
|
|
136
|
+
for union_scope in scope.union_scopes:
|
|
137
|
+
sub_scope_result = scope_cll_map.get(union_scope)
|
|
138
|
+
if sub_scope_result is None:
|
|
139
|
+
raise RecceException(f"Scope {union_scope} not found in scope_cll_map")
|
|
140
|
+
sub_m2c, sub_c2c_map = sub_scope_result
|
|
141
|
+
|
|
142
|
+
for k, v in sub_c2c_map.items():
|
|
143
|
+
if k not in c2c_map:
|
|
144
|
+
c2c_map[k] = v
|
|
145
|
+
else:
|
|
146
|
+
c2c_map[k].depends_on.extend(v.depends_on)
|
|
147
|
+
c2c_map[k].transformation_type = "derived"
|
|
148
|
+
|
|
149
|
+
m2c.extend(sub_m2c)
|
|
150
|
+
return m2c, c2c_map
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cll_select_scope(scope: Scope, scope_cll_map: dict[Scope, CllResult]) -> CllResult:
|
|
154
|
+
assert scope.expression.key == "select"
|
|
155
|
+
|
|
156
|
+
# model-to-column
|
|
157
|
+
m2c: List[CllColumnDep] = []
|
|
158
|
+
# column-to-column
|
|
159
|
+
c2c_map: Dict[str, CllColumn] = {}
|
|
160
|
+
|
|
161
|
+
table_alias_map = {t.alias_or_name: t.name for t in scope.tables}
|
|
162
|
+
select = scope.expression
|
|
163
|
+
|
|
164
|
+
def source_column_dependency(ref_column: exp.Column) -> Optional[CllColumn]:
|
|
165
|
+
column_name = ref_column.name
|
|
166
|
+
table_name = ref_column.table if ref_column.table != "" else next(iter(table_alias_map.values()))
|
|
167
|
+
source = scope.sources.get(table_name, None) # transformation_type: exp.Table | Scope
|
|
168
|
+
if isinstance(source, Scope):
|
|
169
|
+
ref_cll_result = scope_cll_map.get(source)
|
|
170
|
+
if ref_cll_result is None:
|
|
171
|
+
return None
|
|
172
|
+
_, sub_c2c_map = ref_cll_result
|
|
173
|
+
return sub_c2c_map.get(column_name)
|
|
174
|
+
elif isinstance(source, exp.Table):
|
|
175
|
+
return CllColumn(
|
|
176
|
+
name=column_name,
|
|
177
|
+
transformation_type="passthrough",
|
|
178
|
+
depends_on=[CllColumnDep(node=source.name, column=column_name)],
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def subquery_cll(subquery: exp.Subquery) -> Optional[CllResult]:
|
|
184
|
+
select = subquery.find(exp.Select)
|
|
185
|
+
if select is None:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
matched_scope = None
|
|
189
|
+
for sub_scope in scope.subquery_scopes:
|
|
190
|
+
if sub_scope.expression == select:
|
|
191
|
+
matched_scope = sub_scope
|
|
192
|
+
break
|
|
193
|
+
if matched_scope is None:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
return scope_cll_map.get(matched_scope)
|
|
197
|
+
|
|
198
|
+
for proj in scope.expression.selects:
|
|
199
|
+
transformation_type = "source"
|
|
200
|
+
column_depends_on: List[CllColumnDep] = []
|
|
201
|
+
root = proj.this if isinstance(proj, exp.Alias) else proj
|
|
202
|
+
for expression in root.walk(bfs=False):
|
|
203
|
+
if isinstance(expression, exp.Column):
|
|
204
|
+
ref_column_dependency = source_column_dependency(expression)
|
|
205
|
+
if ref_column_dependency is not None:
|
|
206
|
+
column_depends_on.extend(ref_column_dependency.depends_on)
|
|
207
|
+
if ref_column_dependency.transformation_type == "derived":
|
|
208
|
+
transformation_type = "derived"
|
|
209
|
+
elif ref_column_dependency.transformation_type == "renamed":
|
|
210
|
+
if transformation_type == "source" or transformation_type == "passthrough":
|
|
211
|
+
transformation_type = "renamed"
|
|
212
|
+
elif ref_column_dependency.transformation_type == "passthrough":
|
|
213
|
+
if transformation_type == "source":
|
|
214
|
+
transformation_type = "passthrough"
|
|
215
|
+
else:
|
|
216
|
+
column_depends_on.append(CllColumnDep(expression.table, expression.name))
|
|
217
|
+
if transformation_type == "source":
|
|
218
|
+
transformation_type = "passthrough"
|
|
86
219
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
220
|
+
elif isinstance(expression, (exp.Paren, exp.Identifier)):
|
|
221
|
+
pass
|
|
222
|
+
else:
|
|
223
|
+
transformation_type = "derived"
|
|
90
224
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
225
|
+
column_depends_on = _dedeup_depends_on(column_depends_on)
|
|
226
|
+
|
|
227
|
+
if len(column_depends_on) == 0 and transformation_type != "source":
|
|
228
|
+
transformation_type = "source"
|
|
229
|
+
|
|
230
|
+
if isinstance(proj, exp.Alias):
|
|
231
|
+
alias = proj
|
|
232
|
+
if transformation_type == "passthrough" and column_depends_on[0].column != alias.alias_or_name:
|
|
233
|
+
transformation_type = "renamed"
|
|
95
234
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
depends_on=[ColumnLevelDependsOn(table, column.name)]
|
|
235
|
+
c2c_map[proj.alias_or_name] = CllColumn(
|
|
236
|
+
name=proj.alias_or_name, transformation_type=transformation_type, depends_on=column_depends_on
|
|
99
237
|
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
238
|
+
|
|
239
|
+
def selected_column_dependency(ref_column: exp.Column) -> Optional[CllColumn]:
|
|
240
|
+
column_name = ref_column.name
|
|
241
|
+
return c2c_map.get(column_name)
|
|
242
|
+
|
|
243
|
+
# joins clause: Reference the source columns
|
|
244
|
+
if select.args.get("joins"):
|
|
245
|
+
joins = select.args.get("joins")
|
|
246
|
+
for join in joins:
|
|
247
|
+
if isinstance(join, exp.Join):
|
|
248
|
+
for ref_column in join.find_all(exp.Column):
|
|
249
|
+
if source_column_dependency(ref_column) is not None:
|
|
250
|
+
m2c.extend(source_column_dependency(ref_column).depends_on)
|
|
251
|
+
|
|
252
|
+
# where clauses: Reference the source columns
|
|
253
|
+
if select.args.get("where"):
|
|
254
|
+
where = select.args.get("where")
|
|
255
|
+
if isinstance(where, exp.Where):
|
|
256
|
+
for ref_column in where.find_all(exp.Column):
|
|
257
|
+
if source_column_dependency(ref_column) is not None:
|
|
258
|
+
m2c.extend(source_column_dependency(ref_column).depends_on)
|
|
259
|
+
for subquery in where.find_all(exp.Subquery):
|
|
260
|
+
sub_cll = subquery_cll(subquery)
|
|
261
|
+
if sub_cll is not None:
|
|
262
|
+
sub_m2c, sub_c2c_map = sub_cll
|
|
263
|
+
m2c.extend(sub_m2c)
|
|
264
|
+
for sub_c in sub_c2c_map.values():
|
|
265
|
+
m2c.extend(sub_c.depends_on)
|
|
266
|
+
|
|
267
|
+
# group by clause: Reference the source columns, column index
|
|
268
|
+
if select.args.get("group"):
|
|
269
|
+
group = select.args.get("group")
|
|
270
|
+
if isinstance(group, exp.Group):
|
|
271
|
+
for ref_column in group.find_all(exp.Column):
|
|
272
|
+
if source_column_dependency(ref_column) is not None:
|
|
273
|
+
m2c.extend(source_column_dependency(ref_column).depends_on)
|
|
274
|
+
|
|
275
|
+
# having clause: Reference the source columns, selected columns
|
|
276
|
+
if select.args.get("having"):
|
|
277
|
+
having = select.args.get("having")
|
|
278
|
+
if isinstance(having, exp.Having):
|
|
279
|
+
for ref_column in having.find_all(exp.Column):
|
|
280
|
+
if source_column_dependency(ref_column) is not None:
|
|
281
|
+
m2c.extend(source_column_dependency(ref_column).depends_on)
|
|
282
|
+
elif selected_column_dependency(ref_column) is not None:
|
|
283
|
+
m2c.extend(selected_column_dependency(ref_column).depends_on)
|
|
284
|
+
for subquery in having.find_all(exp.Subquery):
|
|
285
|
+
sub_cll = subquery_cll(subquery)
|
|
286
|
+
if sub_cll is not None:
|
|
287
|
+
sub_m2c, sub_c2c_map = sub_cll
|
|
288
|
+
m2c.extend(sub_m2c)
|
|
289
|
+
for sub_c in sub_c2c_map.values():
|
|
290
|
+
m2c.extend(sub_c.depends_on)
|
|
291
|
+
|
|
292
|
+
# order by clause: Reference the source columns, selected columns, column index
|
|
293
|
+
if select.args.get("order"):
|
|
294
|
+
order = select.args.get("order")
|
|
295
|
+
if isinstance(order, exp.Order):
|
|
296
|
+
for ref_column in order.find_all(exp.Column):
|
|
297
|
+
if source_column_dependency(ref_column) is not None:
|
|
298
|
+
m2c.extend(source_column_dependency(ref_column).depends_on)
|
|
299
|
+
elif selected_column_dependency(ref_column) is not None:
|
|
300
|
+
m2c.extend(selected_column_dependency(ref_column).depends_on)
|
|
301
|
+
|
|
302
|
+
for source in scope.sources.values():
|
|
303
|
+
scope_cll_result = scope_cll_map.get(source)
|
|
304
|
+
if scope_cll_result is None:
|
|
305
|
+
continue
|
|
306
|
+
sub_m2c, _ = scope_cll_result
|
|
307
|
+
m2c.extend(sub_m2c)
|
|
308
|
+
|
|
309
|
+
m2c = _dedeup_depends_on(m2c)
|
|
310
|
+
|
|
311
|
+
return m2c, c2c_map
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cll(sql, schema=None, dialect=None) -> CllResult:
|
|
315
|
+
# given a sql, return the cll for the sql
|
|
168
316
|
# {
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
317
|
+
# 'depends_on': [{'node': 'model_id', 'column': 'column'}],
|
|
318
|
+
# 'columns': {
|
|
319
|
+
# 'column1': {
|
|
320
|
+
# 'type': 'derived',
|
|
321
|
+
# 'depends_on': [{'node': 'model_id', 'column': 'column'}],
|
|
322
|
+
# }
|
|
323
|
+
# }
|
|
324
|
+
# }
|
|
174
325
|
|
|
175
326
|
dialect = Dialect.get(dialect) if dialect is not None else None
|
|
176
327
|
|
|
177
328
|
try:
|
|
178
329
|
expression = parse_one(sql, dialect=dialect)
|
|
179
330
|
except SqlglotError as e:
|
|
180
|
-
raise RecceException(f
|
|
331
|
+
raise RecceException(f"Failed to parse SQL: {str(e)}")
|
|
181
332
|
|
|
182
333
|
try:
|
|
183
334
|
expression = qualify(expression, schema=schema, dialect=dialect)
|
|
184
335
|
except OptimizeError as e:
|
|
185
|
-
raise RecceException(f
|
|
336
|
+
raise RecceException(f"Failed to optimize SQL: {str(e)}")
|
|
186
337
|
except SqlglotError as e:
|
|
187
|
-
raise RecceException(f
|
|
338
|
+
raise RecceException(f"Failed to qualify SQL: {str(e)}")
|
|
188
339
|
|
|
189
|
-
result =
|
|
190
|
-
|
|
340
|
+
result = None
|
|
341
|
+
scope_cll_map = {}
|
|
191
342
|
for scope in traverse_scope(expression):
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if isinstance(scope.expression, Union) or isinstance(scope.expression, Intersect):
|
|
200
|
-
for union_scope in scope.union_scopes:
|
|
201
|
-
for k, v in global_lineage[union_scope].items():
|
|
202
|
-
if k not in scope_lineage:
|
|
203
|
-
scope_lineage[k] = v
|
|
204
|
-
else:
|
|
205
|
-
scope_lineage[k].depends_on.extend(v.depends_on)
|
|
206
|
-
scope_lineage[k].type = 'derived'
|
|
343
|
+
scope_type = scope.expression.key
|
|
344
|
+
if scope_type == "union" or scope_type == "intersect" or scope_type == "except":
|
|
345
|
+
result = _cll_set_scope(scope, scope_cll_map)
|
|
346
|
+
elif scope_type == "select":
|
|
347
|
+
result = _cll_select_scope(scope, scope_cll_map)
|
|
207
348
|
else:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# 'select a'
|
|
212
|
-
column = select
|
|
213
|
-
column_cll = _cll_expression(column, table_alias_map)
|
|
214
|
-
elif isinstance(select, Alias):
|
|
215
|
-
# 'select a as b'
|
|
216
|
-
# 'select CURRENT_TIMESTAMP() as create_at'
|
|
217
|
-
alias = select
|
|
218
|
-
col_expression = alias.this
|
|
219
|
-
column_cll = _cll_expression(col_expression, table_alias_map)
|
|
220
|
-
if (
|
|
221
|
-
column_cll and
|
|
222
|
-
column_cll.type == 'passthrough' and
|
|
223
|
-
column_cll.depends_on[0].column != alias.alias_or_name
|
|
224
|
-
):
|
|
225
|
-
column_cll.type = 'renamed'
|
|
226
|
-
else:
|
|
227
|
-
# 'select 1'
|
|
228
|
-
column_cll = ColumnLevelDependencyColumn(type='source', depends_on=[])
|
|
229
|
-
|
|
230
|
-
cte_type = None
|
|
231
|
-
flatten_col_depends_on = []
|
|
232
|
-
for col_dep in column_cll.depends_on:
|
|
233
|
-
col_dep_node = col_dep.node
|
|
234
|
-
col_dep_column = col_dep.column
|
|
235
|
-
# cte
|
|
236
|
-
cte_scope = scope.cte_sources.get(col_dep_node)
|
|
237
|
-
# inline derived table
|
|
238
|
-
source_scope = None
|
|
239
|
-
if isinstance(scope.sources.get(col_dep_node), Scope):
|
|
240
|
-
source_scope = scope.sources.get(col_dep_node)
|
|
241
|
-
|
|
242
|
-
if cte_scope is not None:
|
|
243
|
-
cte_cll = global_lineage[cte_scope]
|
|
244
|
-
if cte_cll is None or cte_cll.get(col_dep_column) is None:
|
|
245
|
-
# In dbt-duckdb, the external source is compiled as `read_csv('..') rather than a table.
|
|
246
|
-
continue
|
|
247
|
-
cte_type = cte_cll.get(col_dep_column).type
|
|
248
|
-
flatten_col_depends_on.extend(cte_cll.get(col_dep_column).depends_on)
|
|
249
|
-
elif source_scope is not None:
|
|
250
|
-
source_cll = global_lineage[source_scope]
|
|
251
|
-
if source_cll is None or source_cll.get(col_dep_column) is None:
|
|
252
|
-
continue
|
|
253
|
-
flatten_col_depends_on.extend(source_cll.get(col_dep_column).depends_on)
|
|
254
|
-
else:
|
|
255
|
-
flatten_col_depends_on.append(col_dep)
|
|
256
|
-
|
|
257
|
-
# deduplicate
|
|
258
|
-
dedup_col_depends_on = []
|
|
259
|
-
dedup_set = set()
|
|
260
|
-
for col_dep in flatten_col_depends_on:
|
|
261
|
-
node_col = col_dep.node + '.' + col_dep.column
|
|
262
|
-
if node_col not in dedup_set:
|
|
263
|
-
dedup_col_depends_on.append(col_dep)
|
|
264
|
-
dedup_set.add(node_col)
|
|
265
|
-
|
|
266
|
-
# transformation type
|
|
267
|
-
type = column_cll.type
|
|
268
|
-
if type == 'derived':
|
|
269
|
-
if len(dedup_col_depends_on) == 0:
|
|
270
|
-
type = 'source'
|
|
271
|
-
else:
|
|
272
|
-
# keep current scope type
|
|
273
|
-
pass
|
|
274
|
-
elif cte_type is not None:
|
|
275
|
-
if len(dedup_col_depends_on) > 1:
|
|
276
|
-
type = 'derived'
|
|
277
|
-
elif len(dedup_col_depends_on) == 0:
|
|
278
|
-
type = 'source'
|
|
279
|
-
else:
|
|
280
|
-
if isinstance(select, Column):
|
|
281
|
-
type = cte_type
|
|
282
|
-
elif isinstance(select, Alias):
|
|
283
|
-
alias = select
|
|
284
|
-
if column_cll.depends_on[0].column == alias.alias_or_name:
|
|
285
|
-
type = cte_type
|
|
286
|
-
else:
|
|
287
|
-
type = 'renamed' if cte_type == 'passthrough' else cte_type
|
|
288
|
-
else:
|
|
289
|
-
type = 'source'
|
|
290
|
-
|
|
291
|
-
scope_lineage[select.alias_or_name] = ColumnLevelDependencyColumn(
|
|
292
|
-
type=type,
|
|
293
|
-
depends_on=dedup_col_depends_on
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
global_lineage[scope] = scope_lineage
|
|
297
|
-
if not scope.is_cte:
|
|
298
|
-
result = scope_lineage
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
scope_cll_map[scope] = result
|
|
299
352
|
|
|
353
|
+
if result is None:
|
|
354
|
+
raise RecceException("Failed to extract CLL from SQL")
|
|
300
355
|
return result
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recce Cloud API client modules.
|
|
3
|
+
|
|
4
|
+
This package provides modular access to Recce Cloud API endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from recce.util.cloud.base import CloudBase
|
|
8
|
+
from recce.util.cloud.check_events import CheckEventsCloud
|
|
9
|
+
from recce.util.cloud.checks import ChecksCloud
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CloudBase",
|
|
13
|
+
"CheckEventsCloud",
|
|
14
|
+
"ChecksCloud",
|
|
15
|
+
]
|
recce/util/cloud/base.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for Recce Cloud API clients.
|
|
3
|
+
|
|
4
|
+
This module provides the common functionality shared across all cloud API clients.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from recce.util.recce_cloud import (
|
|
13
|
+
DOCKER_INTERNAL_URL_PREFIX,
|
|
14
|
+
LOCALHOST_URL_PREFIX,
|
|
15
|
+
RECCE_CLOUD_API_HOST,
|
|
16
|
+
RecceCloudException,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudBase:
|
|
21
|
+
"""
|
|
22
|
+
Base class for Recce Cloud API operations.
|
|
23
|
+
|
|
24
|
+
Provides common functionality for making authenticated requests to the Recce Cloud API,
|
|
25
|
+
including request handling, error management, and Docker environment URL conversion.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
token: Authentication token (API token or GitHub token)
|
|
29
|
+
token_type: Type of token ("api_token" or "github_token")
|
|
30
|
+
base_url: Base URL for API v1 endpoints
|
|
31
|
+
base_url_v2: Base URL for API v2 endpoints
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, token: str):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the CloudBase client.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
token: Authentication token for Recce Cloud API
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If token is None
|
|
43
|
+
"""
|
|
44
|
+
if token is None:
|
|
45
|
+
raise ValueError("Token cannot be None.")
|
|
46
|
+
|
|
47
|
+
self.token = token
|
|
48
|
+
self.token_type = "github_token" if token.startswith(("ghp_", "gho_", "ghu_", "ghs_", "ghr_")) else "api_token"
|
|
49
|
+
self.base_url = f"{RECCE_CLOUD_API_HOST}/api/v1"
|
|
50
|
+
self.base_url_v2 = f"{RECCE_CLOUD_API_HOST}/api/v2"
|
|
51
|
+
|
|
52
|
+
def _request(self, method: str, url: str, headers: Optional[Dict] = None, **kwargs):
|
|
53
|
+
"""
|
|
54
|
+
Make an authenticated HTTP request to Recce Cloud API.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
method: HTTP method (GET, POST, PATCH, DELETE, etc.)
|
|
58
|
+
url: Full URL for the request
|
|
59
|
+
headers: Optional additional headers
|
|
60
|
+
**kwargs: Additional arguments passed to requests.request
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Response object from requests library
|
|
64
|
+
"""
|
|
65
|
+
headers = {
|
|
66
|
+
**(headers or {}),
|
|
67
|
+
"Authorization": f"Bearer {self.token}",
|
|
68
|
+
}
|
|
69
|
+
url = self._replace_localhost_with_docker_internal(url)
|
|
70
|
+
return requests.request(method, url, headers=headers, **kwargs)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _replace_localhost_with_docker_internal(url: str) -> Optional[str]:
|
|
74
|
+
"""
|
|
75
|
+
Convert localhost URLs to docker internal URLs if running in Docker.
|
|
76
|
+
|
|
77
|
+
This is useful for local development when Recce is running inside a Docker container
|
|
78
|
+
and needs to access localhost services on the host machine.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
url: URL that might contain localhost
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
URL with localhost replaced by host.docker.internal if in Docker, otherwise original URL
|
|
85
|
+
"""
|
|
86
|
+
if url is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
os.environ.get("RECCE_SHARE_INSTANCE_ENV") == "docker"
|
|
91
|
+
or os.environ.get("RECCE_TASK_INSTANCE_ENV") == "docker"
|
|
92
|
+
or os.environ.get("RECCE_INSTANCE_ENV") == "docker"
|
|
93
|
+
):
|
|
94
|
+
if url.startswith(LOCALHOST_URL_PREFIX):
|
|
95
|
+
return url.replace(LOCALHOST_URL_PREFIX, DOCKER_INTERNAL_URL_PREFIX)
|
|
96
|
+
|
|
97
|
+
return url
|
|
98
|
+
|
|
99
|
+
def _raise_for_status(self, response, message: str):
|
|
100
|
+
"""
|
|
101
|
+
Raise RecceCloudException if the response status is not successful.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
response: Response object from requests
|
|
105
|
+
message: Error message to include in the exception
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
RecceCloudException: If response status code is not 2xx
|
|
109
|
+
"""
|
|
110
|
+
if not response.ok:
|
|
111
|
+
raise RecceCloudException(
|
|
112
|
+
message=message,
|
|
113
|
+
reason=response.text,
|
|
114
|
+
status_code=response.status_code,
|
|
115
|
+
)
|