recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__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 +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.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/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +8 -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 +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- 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 +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -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 +188 -143
- 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 +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- 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 +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +76 -0
- recce_cloud/api/gitlab.py +82 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- 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/368-7587b306577df275.js +0 -65
- 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/3a92ee20-3b5d922d4157af5e.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/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.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/8d700b6a-f0b1f6b9e0d97ce2.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-cee661090afbd6aa.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/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for platform-specific Recce Cloud API clients.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from recce_cloud.api.factory import create_platform_client
|
|
12
|
+
from recce_cloud.api.github import GitHubRecceCloudClient
|
|
13
|
+
from recce_cloud.api.gitlab import GitLabRecceCloudClient
|
|
14
|
+
from recce_cloud.ci_providers.base import CIInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestGitHubRecceCloudClient:
|
|
18
|
+
"""Tests for GitHub Actions API client."""
|
|
19
|
+
|
|
20
|
+
def test_init(self):
|
|
21
|
+
"""Test client initialization."""
|
|
22
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
23
|
+
assert client.token == "test_token"
|
|
24
|
+
assert client.repository == "owner/repo"
|
|
25
|
+
parsed = urlparse(client.api_host)
|
|
26
|
+
# Accept main domain or subdomains:
|
|
27
|
+
assert parsed.hostname == "cloud.datarecce.io" or (
|
|
28
|
+
parsed.hostname and parsed.hostname.endswith(".cloud.datarecce.io")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_touch_recce_session_pr(self):
|
|
32
|
+
"""Test touch_recce_session for PR context."""
|
|
33
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
34
|
+
|
|
35
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
36
|
+
mock_request.return_value = {
|
|
37
|
+
"session_id": "test_session_id",
|
|
38
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
39
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
response = client.touch_recce_session(
|
|
43
|
+
branch="feature-branch", adapter_type="postgres", cr_number=123, session_type="cr"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert response["session_id"] == "test_session_id"
|
|
47
|
+
assert response["manifest_upload_url"] == "https://s3.aws.com/manifest"
|
|
48
|
+
assert response["catalog_upload_url"] == "https://s3.aws.com/catalog"
|
|
49
|
+
|
|
50
|
+
# Verify correct API endpoint was called
|
|
51
|
+
mock_request.assert_called_once()
|
|
52
|
+
call_args = mock_request.call_args
|
|
53
|
+
assert call_args[0][0] == "POST"
|
|
54
|
+
assert "github/owner/repo/touch-recce-session" in call_args[0][1]
|
|
55
|
+
assert call_args[1]["json"]["branch"] == "feature-branch"
|
|
56
|
+
assert call_args[1]["json"]["adapter_type"] == "postgres"
|
|
57
|
+
assert call_args[1]["json"]["pr_number"] == 123
|
|
58
|
+
|
|
59
|
+
def test_touch_recce_session_base(self):
|
|
60
|
+
"""Test touch_recce_session for base branch context."""
|
|
61
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
62
|
+
|
|
63
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
64
|
+
mock_request.return_value = {
|
|
65
|
+
"session_id": "base_session_id",
|
|
66
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
67
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
client.touch_recce_session(branch="main", adapter_type="snowflake")
|
|
71
|
+
|
|
72
|
+
# Verify pr_number is not in the payload when cr_number is None
|
|
73
|
+
call_args = mock_request.call_args
|
|
74
|
+
assert "pr_number" not in call_args[1]["json"]
|
|
75
|
+
|
|
76
|
+
def test_touch_recce_session_prod_type(self):
|
|
77
|
+
"""Test touch_recce_session with --type prod (should not include pr_number)."""
|
|
78
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
79
|
+
|
|
80
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
81
|
+
mock_request.return_value = {
|
|
82
|
+
"session_id": "prod_session_id",
|
|
83
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
84
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Even with cr_number detected, session_type="prod" should omit pr_number
|
|
88
|
+
client.touch_recce_session(branch="main", adapter_type="postgres", cr_number=123, session_type="prod")
|
|
89
|
+
|
|
90
|
+
# Verify pr_number is NOT in the payload when session_type is "prod"
|
|
91
|
+
call_args = mock_request.call_args
|
|
92
|
+
assert "pr_number" not in call_args[1]["json"]
|
|
93
|
+
assert call_args[1]["json"]["branch"] == "main"
|
|
94
|
+
assert call_args[1]["json"]["adapter_type"] == "postgres"
|
|
95
|
+
|
|
96
|
+
def test_upload_completed(self):
|
|
97
|
+
"""Test upload_completed notification."""
|
|
98
|
+
client = GitHubRecceCloudClient(token="test_token", repository="owner/repo")
|
|
99
|
+
|
|
100
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
101
|
+
mock_request.return_value = {}
|
|
102
|
+
|
|
103
|
+
client.upload_completed(session_id="test_session_id")
|
|
104
|
+
|
|
105
|
+
mock_request.assert_called_once()
|
|
106
|
+
call_args = mock_request.call_args
|
|
107
|
+
assert call_args[0][0] == "POST"
|
|
108
|
+
assert "github/owner/repo/upload-completed" in call_args[0][1]
|
|
109
|
+
assert call_args[1]["json"]["session_id"] == "test_session_id"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestGitLabRecceCloudClient:
|
|
113
|
+
"""Tests for GitLab CI API client."""
|
|
114
|
+
|
|
115
|
+
def test_init(self):
|
|
116
|
+
"""Test client initialization."""
|
|
117
|
+
client = GitLabRecceCloudClient(
|
|
118
|
+
token="test_token",
|
|
119
|
+
project_path="group/project",
|
|
120
|
+
repository_url="https://gitlab.com/group/project",
|
|
121
|
+
)
|
|
122
|
+
assert client.token == "test_token"
|
|
123
|
+
assert client.project_path == "group/project"
|
|
124
|
+
assert client.repository_url == "https://gitlab.com/group/project"
|
|
125
|
+
|
|
126
|
+
def test_touch_recce_session_mr(self):
|
|
127
|
+
"""Test touch_recce_session for MR context."""
|
|
128
|
+
client = GitLabRecceCloudClient(
|
|
129
|
+
token="test_token",
|
|
130
|
+
project_path="group/project",
|
|
131
|
+
repository_url="https://gitlab.com/group/project",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
135
|
+
mock_request.return_value = {
|
|
136
|
+
"session_id": "test_session_id",
|
|
137
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
138
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
response = client.touch_recce_session(
|
|
142
|
+
branch="feature-branch",
|
|
143
|
+
adapter_type="postgres",
|
|
144
|
+
cr_number=456,
|
|
145
|
+
commit_sha="abc123def456",
|
|
146
|
+
session_type="cr",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
assert response["session_id"] == "test_session_id"
|
|
150
|
+
|
|
151
|
+
# Verify correct API endpoint and payload
|
|
152
|
+
call_args = mock_request.call_args
|
|
153
|
+
assert call_args[0][0] == "POST"
|
|
154
|
+
assert "gitlab/group/project/touch-recce-session" in call_args[0][1]
|
|
155
|
+
payload = call_args[1]["json"]
|
|
156
|
+
assert payload["branch"] == "feature-branch"
|
|
157
|
+
assert payload["adapter_type"] == "postgres"
|
|
158
|
+
assert payload["mr_iid"] == 456
|
|
159
|
+
assert payload["commit_sha"] == "abc123def456"
|
|
160
|
+
assert payload["repository_url"] == "https://gitlab.com/group/project"
|
|
161
|
+
|
|
162
|
+
def test_touch_recce_session_base(self):
|
|
163
|
+
"""Test touch_recce_session for base branch context."""
|
|
164
|
+
client = GitLabRecceCloudClient(
|
|
165
|
+
token="test_token",
|
|
166
|
+
project_path="group/project",
|
|
167
|
+
repository_url="https://gitlab.com/group/project",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
171
|
+
mock_request.return_value = {
|
|
172
|
+
"session_id": "base_session_id",
|
|
173
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
174
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
client.touch_recce_session(branch="main", adapter_type="bigquery", commit_sha="base123")
|
|
178
|
+
|
|
179
|
+
# Verify mr_iid is not in the payload when cr_number is None
|
|
180
|
+
call_args = mock_request.call_args
|
|
181
|
+
assert "mr_iid" not in call_args[1]["json"]
|
|
182
|
+
|
|
183
|
+
def test_touch_recce_session_prod_type(self):
|
|
184
|
+
"""Test touch_recce_session with --type prod (should not include mr_iid)."""
|
|
185
|
+
client = GitLabRecceCloudClient(
|
|
186
|
+
token="test_token",
|
|
187
|
+
project_path="group/project",
|
|
188
|
+
repository_url="https://gitlab.com/group/project",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
192
|
+
mock_request.return_value = {
|
|
193
|
+
"session_id": "prod_session_id",
|
|
194
|
+
"manifest_upload_url": "https://s3.aws.com/manifest",
|
|
195
|
+
"catalog_upload_url": "https://s3.aws.com/catalog",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Even with cr_number detected, session_type="prod" should omit mr_iid
|
|
199
|
+
client.touch_recce_session(
|
|
200
|
+
branch="main",
|
|
201
|
+
adapter_type="postgres",
|
|
202
|
+
cr_number=456,
|
|
203
|
+
commit_sha="abc123def456",
|
|
204
|
+
session_type="prod",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Verify mr_iid is NOT in the payload when session_type is "prod"
|
|
208
|
+
call_args = mock_request.call_args
|
|
209
|
+
assert "mr_iid" not in call_args[1]["json"]
|
|
210
|
+
assert call_args[1]["json"]["branch"] == "main"
|
|
211
|
+
assert call_args[1]["json"]["adapter_type"] == "postgres"
|
|
212
|
+
assert call_args[1]["json"]["commit_sha"] == "abc123def456"
|
|
213
|
+
|
|
214
|
+
def test_upload_completed(self):
|
|
215
|
+
"""Test upload_completed notification."""
|
|
216
|
+
client = GitLabRecceCloudClient(
|
|
217
|
+
token="test_token",
|
|
218
|
+
project_path="group/project",
|
|
219
|
+
repository_url="https://gitlab.com/group/project",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
with patch.object(client, "_make_request") as mock_request:
|
|
223
|
+
mock_request.return_value = {}
|
|
224
|
+
|
|
225
|
+
client.upload_completed(session_id="test_session_id", commit_sha="commit456")
|
|
226
|
+
|
|
227
|
+
mock_request.assert_called_once()
|
|
228
|
+
call_args = mock_request.call_args
|
|
229
|
+
assert call_args[0][0] == "POST"
|
|
230
|
+
assert "gitlab/group/project/upload-completed" in call_args[0][1]
|
|
231
|
+
payload = call_args[1]["json"]
|
|
232
|
+
assert payload["session_id"] == "test_session_id"
|
|
233
|
+
assert payload["commit_sha"] == "commit456"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class TestFactoryCreatePlatformClient:
|
|
237
|
+
"""Tests for create_platform_client factory function."""
|
|
238
|
+
|
|
239
|
+
def test_create_github_client(self):
|
|
240
|
+
"""Test creating GitHub client."""
|
|
241
|
+
ci_info = CIInfo(platform="github-actions", repository="owner/repo")
|
|
242
|
+
|
|
243
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
244
|
+
|
|
245
|
+
assert isinstance(client, GitHubRecceCloudClient)
|
|
246
|
+
assert client.repository == "owner/repo"
|
|
247
|
+
|
|
248
|
+
def test_create_github_client_from_env(self):
|
|
249
|
+
"""Test creating GitHub client with environment fallback."""
|
|
250
|
+
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "owner/repo"}):
|
|
251
|
+
ci_info = CIInfo(platform="github-actions")
|
|
252
|
+
|
|
253
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
254
|
+
|
|
255
|
+
assert isinstance(client, GitHubRecceCloudClient)
|
|
256
|
+
assert client.repository == "owner/repo"
|
|
257
|
+
|
|
258
|
+
def test_create_github_client_missing_repository(self):
|
|
259
|
+
"""Test error when GitHub repository information is missing."""
|
|
260
|
+
ci_info = CIInfo(platform="github-actions")
|
|
261
|
+
|
|
262
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
263
|
+
with pytest.raises(ValueError, match="GitHub repository information is required"):
|
|
264
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
265
|
+
|
|
266
|
+
def test_create_gitlab_client(self):
|
|
267
|
+
"""Test creating GitLab client."""
|
|
268
|
+
with patch.dict(
|
|
269
|
+
os.environ,
|
|
270
|
+
{"CI_PROJECT_URL": "https://gitlab.com/group/project"},
|
|
271
|
+
):
|
|
272
|
+
ci_info = CIInfo(platform="gitlab-ci", repository="group/project")
|
|
273
|
+
|
|
274
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
275
|
+
|
|
276
|
+
assert isinstance(client, GitLabRecceCloudClient)
|
|
277
|
+
assert client.project_path == "group/project"
|
|
278
|
+
assert client.repository_url == "https://gitlab.com/group/project"
|
|
279
|
+
|
|
280
|
+
def test_create_gitlab_client_from_env(self):
|
|
281
|
+
"""Test creating GitLab client with environment fallback."""
|
|
282
|
+
with patch.dict(
|
|
283
|
+
os.environ,
|
|
284
|
+
{
|
|
285
|
+
"CI_PROJECT_PATH": "group/project",
|
|
286
|
+
"CI_PROJECT_URL": "https://gitlab.com/group/project",
|
|
287
|
+
},
|
|
288
|
+
):
|
|
289
|
+
ci_info = CIInfo(platform="gitlab-ci")
|
|
290
|
+
|
|
291
|
+
client = create_platform_client(token="test_token", ci_info=ci_info)
|
|
292
|
+
|
|
293
|
+
assert isinstance(client, GitLabRecceCloudClient)
|
|
294
|
+
assert client.project_path == "group/project"
|
|
295
|
+
|
|
296
|
+
def test_create_gitlab_client_missing_project_path(self):
|
|
297
|
+
"""Test error when GitLab project path is missing."""
|
|
298
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
299
|
+
ci_info = CIInfo(platform="gitlab-ci")
|
|
300
|
+
|
|
301
|
+
with pytest.raises(ValueError, match="GitLab project path is required"):
|
|
302
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
303
|
+
|
|
304
|
+
def test_create_gitlab_client_missing_project_url(self):
|
|
305
|
+
"""Test error when GitLab project URL is missing."""
|
|
306
|
+
with patch.dict(os.environ, {"CI_PROJECT_PATH": "group/project"}, clear=True):
|
|
307
|
+
ci_info = CIInfo(platform="gitlab-ci", repository="group/project")
|
|
308
|
+
|
|
309
|
+
with pytest.raises(ValueError, match="GitLab project URL is required"):
|
|
310
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
311
|
+
|
|
312
|
+
def test_create_client_unsupported_platform(self):
|
|
313
|
+
"""Test error for unsupported platform."""
|
|
314
|
+
ci_info = CIInfo(platform="unsupported-ci")
|
|
315
|
+
|
|
316
|
+
with pytest.raises(ValueError, match="Unsupported platform"):
|
|
317
|
+
create_platform_client(token="test_token", ci_info=ci_info)
|
|
318
|
+
|
|
319
|
+
def test_auto_detect_ci_info(self):
|
|
320
|
+
"""Test automatic CI detection when ci_info is not provided."""
|
|
321
|
+
with patch.dict(
|
|
322
|
+
os.environ,
|
|
323
|
+
{
|
|
324
|
+
"GITHUB_ACTIONS": "true",
|
|
325
|
+
"GITHUB_REPOSITORY": "owner/repo",
|
|
326
|
+
"GITHUB_SHA": "abc123",
|
|
327
|
+
},
|
|
328
|
+
clear=True,
|
|
329
|
+
):
|
|
330
|
+
client = create_platform_client(token="test_token")
|
|
331
|
+
|
|
332
|
+
assert isinstance(client, GitHubRecceCloudClient)
|
|
333
|
+
assert client.repository == "owner/repo"
|
tests/tasks/conftest.py
CHANGED
tests/tasks/test_histogram.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from recce.tasks.histogram import
|
|
3
|
+
from recce.tasks.histogram import (
|
|
4
|
+
HistogramDiffCheckValidator,
|
|
5
|
+
HistogramDiffTask,
|
|
6
|
+
_is_histogram_supported,
|
|
7
|
+
)
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
def test_histogram(dbt_test_helper):
|
|
@@ -14,11 +18,7 @@ def test_histogram(dbt_test_helper):
|
|
|
14
18
|
|
|
15
19
|
dbt_test_helper.create_model("customers", csv_data, csv_data)
|
|
16
20
|
|
|
17
|
-
params = {
|
|
18
|
-
"model": "customers",
|
|
19
|
-
"column_name": "age",
|
|
20
|
-
"column_type": "int"
|
|
21
|
-
}
|
|
21
|
+
params = {"model": "customers", "column_name": "age", "column_type": "int"}
|
|
22
22
|
|
|
23
23
|
task = HistogramDiffTask(params)
|
|
24
24
|
run_result = task.execute()
|
|
@@ -30,13 +30,13 @@ def test_histogram(dbt_test_helper):
|
|
|
30
30
|
# 'bin_edges': [25, 26, ..., 51],
|
|
31
31
|
# 'labels': ['25-26', ..., '51-52']
|
|
32
32
|
# }
|
|
33
|
-
assert run_result[
|
|
34
|
-
assert run_result[
|
|
35
|
-
assert run_result[
|
|
36
|
-
assert run_result[
|
|
37
|
-
assert run_result[
|
|
38
|
-
assert run_result[
|
|
39
|
-
assert run_result[
|
|
33
|
+
assert run_result["current"]["counts"][0] == 1
|
|
34
|
+
assert run_result["current"]["counts"][-1] == 1
|
|
35
|
+
assert run_result["current"]["total"] == 4
|
|
36
|
+
assert run_result["min"] == 25
|
|
37
|
+
assert run_result["max"] == 50
|
|
38
|
+
assert run_result["bin_edges"][0] == 25
|
|
39
|
+
assert run_result["bin_edges"][-1] == 51
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def test_histogram_emtpy(dbt_test_helper):
|
|
@@ -56,74 +56,66 @@ def test_histogram_emtpy(dbt_test_helper):
|
|
|
56
56
|
dbt_test_helper.create_model("customers2", csv_data, csv_data_zero)
|
|
57
57
|
dbt_test_helper.create_model("customers3", csv_data_zero, csv_data)
|
|
58
58
|
|
|
59
|
-
params = {
|
|
60
|
-
"model": "customers",
|
|
61
|
-
"column_name": "age",
|
|
62
|
-
"column_type": "int"
|
|
63
|
-
}
|
|
59
|
+
params = {"model": "customers", "column_name": "age", "column_type": "int"}
|
|
64
60
|
|
|
65
61
|
task = HistogramDiffTask(params)
|
|
66
62
|
run_result = task.execute()
|
|
67
63
|
|
|
68
|
-
assert len(run_result[
|
|
69
|
-
assert run_result[
|
|
70
|
-
assert run_result[
|
|
71
|
-
assert run_result[
|
|
72
|
-
assert len(run_result[
|
|
64
|
+
assert len(run_result["current"]["counts"]) == 0
|
|
65
|
+
assert run_result["current"]["total"] == 0
|
|
66
|
+
assert run_result["min"] is None
|
|
67
|
+
assert run_result["max"] is None
|
|
68
|
+
assert len(run_result["bin_edges"]) == 0
|
|
73
69
|
|
|
74
|
-
params = {
|
|
75
|
-
"model": "customers2",
|
|
76
|
-
"column_name": "age",
|
|
77
|
-
"column_type": "int"
|
|
78
|
-
}
|
|
70
|
+
params = {"model": "customers2", "column_name": "age", "column_type": "int"}
|
|
79
71
|
|
|
80
72
|
task = HistogramDiffTask(params)
|
|
81
73
|
run_result = task.execute()
|
|
82
|
-
assert run_result[
|
|
83
|
-
assert run_result[
|
|
84
|
-
assert run_result[
|
|
85
|
-
assert run_result[
|
|
86
|
-
assert run_result[
|
|
87
|
-
assert run_result[
|
|
88
|
-
assert run_result[
|
|
89
|
-
assert run_result[
|
|
90
|
-
assert run_result[
|
|
91
|
-
assert run_result[
|
|
92
|
-
|
|
93
|
-
params = {
|
|
94
|
-
"model": "customers3",
|
|
95
|
-
"column_name": "age",
|
|
96
|
-
"column_type": "int"
|
|
97
|
-
}
|
|
74
|
+
assert run_result["base"]["counts"][0] == 1
|
|
75
|
+
assert run_result["base"]["counts"][-1] == 1
|
|
76
|
+
assert run_result["base"]["total"] == 4
|
|
77
|
+
assert run_result["current"]["counts"][0] == 0
|
|
78
|
+
assert run_result["current"]["counts"][-1] == 0
|
|
79
|
+
assert run_result["current"]["total"] == 0
|
|
80
|
+
assert run_result["min"] == 25
|
|
81
|
+
assert run_result["max"] == 50
|
|
82
|
+
assert run_result["bin_edges"][0] == 25
|
|
83
|
+
assert run_result["bin_edges"][-1] == 51
|
|
84
|
+
|
|
85
|
+
params = {"model": "customers3", "column_name": "age", "column_type": "int"}
|
|
98
86
|
|
|
99
87
|
task = HistogramDiffTask(params)
|
|
100
88
|
run_result = task.execute()
|
|
101
|
-
assert run_result[
|
|
102
|
-
assert run_result[
|
|
103
|
-
assert run_result[
|
|
104
|
-
assert run_result[
|
|
105
|
-
assert run_result[
|
|
106
|
-
assert run_result[
|
|
107
|
-
assert run_result[
|
|
108
|
-
assert run_result[
|
|
109
|
-
assert run_result[
|
|
110
|
-
assert run_result[
|
|
89
|
+
assert run_result["base"]["counts"][0] == 0
|
|
90
|
+
assert run_result["base"]["counts"][-1] == 0
|
|
91
|
+
assert run_result["base"]["total"] == 0
|
|
92
|
+
assert run_result["current"]["counts"][0] == 1
|
|
93
|
+
assert run_result["current"]["counts"][-1] == 1
|
|
94
|
+
assert run_result["current"]["total"] == 4
|
|
95
|
+
assert run_result["min"] == 25
|
|
96
|
+
assert run_result["max"] == 50
|
|
97
|
+
assert run_result["bin_edges"][0] == 25
|
|
98
|
+
assert run_result["bin_edges"][-1] == 51
|
|
111
99
|
|
|
112
100
|
|
|
113
101
|
def test_validator():
|
|
114
102
|
def validate(params: dict = {}, view_options: dict = {}):
|
|
115
|
-
HistogramDiffCheckValidator().validate(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
103
|
+
HistogramDiffCheckValidator().validate(
|
|
104
|
+
{
|
|
105
|
+
"name": "test",
|
|
106
|
+
"type": "histogram_diff",
|
|
107
|
+
"params": params,
|
|
108
|
+
"view_options": view_options,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
validate(
|
|
113
|
+
{
|
|
114
|
+
"model": "customers",
|
|
115
|
+
"column_name": "age",
|
|
116
|
+
"column_type": "int",
|
|
117
|
+
}
|
|
118
|
+
)
|
|
127
119
|
|
|
128
120
|
with pytest.raises(ValueError):
|
|
129
121
|
validate({})
|
tests/tasks/test_lineage.py
CHANGED
|
@@ -3,40 +3,53 @@ import pytest
|
|
|
3
3
|
|
|
4
4
|
def test_validator():
|
|
5
5
|
from recce.tasks.lineage import LineageDiffCheckValidator
|
|
6
|
+
|
|
6
7
|
validator = LineageDiffCheckValidator()
|
|
7
8
|
|
|
8
9
|
def validate(params: dict):
|
|
9
|
-
validator.validate(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
validator.validate(
|
|
11
|
+
{
|
|
12
|
+
"name": "test",
|
|
13
|
+
"type": "schema_diff",
|
|
14
|
+
"params": params,
|
|
15
|
+
}
|
|
16
|
+
)
|
|
14
17
|
|
|
15
18
|
# Select all models
|
|
16
19
|
validate({})
|
|
17
20
|
|
|
18
21
|
# Select by selector
|
|
19
|
-
validate(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
validate(
|
|
23
|
+
{
|
|
24
|
+
"select": "customers",
|
|
25
|
+
"exclude": "customers",
|
|
26
|
+
"packages": ["jaffle_shop"],
|
|
27
|
+
"view_mode": "all",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
25
30
|
|
|
26
31
|
# packages should be an array
|
|
27
32
|
with pytest.raises(ValueError):
|
|
28
|
-
validate(
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
validate(
|
|
34
|
+
{
|
|
35
|
+
"packages": "jaffle_shop",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
31
38
|
|
|
32
39
|
# view_mode should be 'all' or 'changed_models'
|
|
33
|
-
validate(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
validate(
|
|
41
|
+
{
|
|
42
|
+
"view_mode": None,
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
validate(
|
|
46
|
+
{
|
|
47
|
+
"view_mode": "all",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
39
50
|
with pytest.raises(ValueError):
|
|
40
|
-
validate(
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
validate(
|
|
52
|
+
{
|
|
53
|
+
"view_mode": "abc",
|
|
54
|
+
}
|
|
55
|
+
)
|