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.

Files changed (213) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +810 -480
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +39 -28
  8. recce/apis/check_func.py +33 -27
  9. recce/apis/run_api.py +25 -19
  10. recce/apis/run_func.py +29 -23
  11. recce/artifact.py +119 -51
  12. recce/cli.py +1299 -323
  13. recce/config.py +42 -33
  14. recce/connect_to_cloud.py +138 -0
  15. recce/core.py +55 -47
  16. recce/data/404.html +1 -1
  17. recce/data/__next.__PAGE__.txt +10 -0
  18. recce/data/__next._full.txt +23 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +8 -0
  21. recce/data/__next._tree.txt +5 -0
  22. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
  23. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
  24. recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
  25. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  26. recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
  27. recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
  28. recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
  29. recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
  30. recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
  31. recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
  32. recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
  33. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  34. recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
  35. recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
  36. recce/data/_next/static/chunks/99d638224186c118.js +1 -0
  37. recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
  38. recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
  39. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  40. recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
  41. recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
  42. recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
  43. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  44. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  45. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  46. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  47. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  48. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  49. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  50. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  51. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  52. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  53. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  54. recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
  55. recce/data/_not-found/__next._full.txt +17 -0
  56. recce/data/_not-found/__next._head.txt +8 -0
  57. recce/data/_not-found/__next._index.txt +8 -0
  58. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  59. recce/data/_not-found/__next._not-found.txt +4 -0
  60. recce/data/_not-found/__next._tree.txt +3 -0
  61. recce/data/_not-found.html +1 -0
  62. recce/data/_not-found.txt +17 -0
  63. recce/data/auth_callback.html +68 -0
  64. recce/data/imgs/reload-image.svg +4 -0
  65. recce/data/index.html +1 -27
  66. recce/data/index.txt +23 -7
  67. recce/diff.py +6 -12
  68. recce/event/__init__.py +86 -74
  69. recce/event/collector.py +33 -22
  70. recce/event/track.py +49 -27
  71. recce/exceptions.py +1 -1
  72. recce/git.py +7 -7
  73. recce/github.py +57 -53
  74. recce/mcp_server.py +716 -0
  75. recce/models/__init__.py +4 -1
  76. recce/models/check.py +6 -7
  77. recce/models/run.py +1 -0
  78. recce/models/types.py +131 -28
  79. recce/pull_request.py +27 -25
  80. recce/run.py +165 -121
  81. recce/server.py +303 -111
  82. recce/state/__init__.py +31 -0
  83. recce/state/cloud.py +632 -0
  84. recce/state/const.py +26 -0
  85. recce/state/local.py +56 -0
  86. recce/state/state.py +119 -0
  87. recce/state/state_loader.py +174 -0
  88. recce/summary.py +188 -143
  89. recce/tasks/__init__.py +19 -3
  90. recce/tasks/core.py +11 -13
  91. recce/tasks/dataframe.py +82 -18
  92. recce/tasks/histogram.py +69 -34
  93. recce/tasks/lineage.py +2 -2
  94. recce/tasks/profile.py +152 -86
  95. recce/tasks/query.py +139 -87
  96. recce/tasks/rowcount.py +37 -31
  97. recce/tasks/schema.py +18 -15
  98. recce/tasks/top_k.py +35 -35
  99. recce/tasks/valuediff.py +216 -152
  100. recce/util/__init__.py +3 -0
  101. recce/util/api_token.py +80 -0
  102. recce/util/breaking.py +87 -85
  103. recce/util/cll.py +274 -219
  104. recce/util/io.py +22 -17
  105. recce/util/lineage.py +65 -16
  106. recce/util/logger.py +1 -1
  107. recce/util/onboarding_state.py +45 -0
  108. recce/util/perf_tracking.py +85 -0
  109. recce/util/recce_cloud.py +322 -72
  110. recce/util/singleton.py +4 -4
  111. recce/yaml/__init__.py +7 -10
  112. recce_cloud/__init__.py +24 -0
  113. recce_cloud/api/__init__.py +17 -0
  114. recce_cloud/api/base.py +111 -0
  115. recce_cloud/api/client.py +150 -0
  116. recce_cloud/api/exceptions.py +26 -0
  117. recce_cloud/api/factory.py +63 -0
  118. recce_cloud/api/github.py +76 -0
  119. recce_cloud/api/gitlab.py +82 -0
  120. recce_cloud/artifact.py +57 -0
  121. recce_cloud/ci_providers/__init__.py +9 -0
  122. recce_cloud/ci_providers/base.py +82 -0
  123. recce_cloud/ci_providers/detector.py +147 -0
  124. recce_cloud/ci_providers/github_actions.py +136 -0
  125. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  126. recce_cloud/cli.py +245 -0
  127. recce_cloud/upload.py +214 -0
  128. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
  129. recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
  130. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
  131. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
  132. tests/adapter/dbt_adapter/conftest.py +9 -5
  133. tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
  134. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
  135. tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
  136. tests/adapter/dbt_adapter/test_selector.py +22 -21
  137. tests/recce_cloud/__init__.py +0 -0
  138. tests/recce_cloud/test_ci_providers.py +351 -0
  139. tests/recce_cloud/test_cli.py +372 -0
  140. tests/recce_cloud/test_client.py +273 -0
  141. tests/recce_cloud/test_platform_clients.py +333 -0
  142. tests/tasks/conftest.py +1 -1
  143. tests/tasks/test_histogram.py +58 -66
  144. tests/tasks/test_lineage.py +36 -23
  145. tests/tasks/test_preset_checks.py +45 -31
  146. tests/tasks/test_profile.py +339 -15
  147. tests/tasks/test_query.py +46 -46
  148. tests/tasks/test_row_count.py +65 -46
  149. tests/tasks/test_schema.py +65 -42
  150. tests/tasks/test_top_k.py +22 -18
  151. tests/tasks/test_valuediff.py +43 -32
  152. tests/test_cli.py +174 -60
  153. tests/test_cli_mcp_optional.py +45 -0
  154. tests/test_cloud_listing_cli.py +324 -0
  155. tests/test_config.py +7 -9
  156. tests/test_connect_to_cloud.py +82 -0
  157. tests/test_core.py +151 -4
  158. tests/test_dbt.py +7 -7
  159. tests/test_mcp_server.py +332 -0
  160. tests/test_pull_request.py +1 -1
  161. tests/test_server.py +25 -19
  162. tests/test_summary.py +29 -17
  163. recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
  164. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  165. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  166. recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
  167. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  168. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  169. recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
  170. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  171. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  172. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  173. recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
  174. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  175. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  176. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  177. recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
  178. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  179. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  180. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  181. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  182. recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
  183. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  184. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  185. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  186. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  187. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  188. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  189. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  190. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  191. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  192. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  193. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  194. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  195. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  196. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  197. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  198. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  199. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  200. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  202. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  203. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  205. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  206. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  207. recce/state.py +0 -753
  208. recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
  209. tests/test_state.py +0 -123
  210. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
  211. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  212. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
  213. {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
@@ -1,4 +1,4 @@
1
1
  # Share the fixtures for the tasks tests
2
2
 
3
3
  # noinspection PyUnresolvedReferences
4
- from tests.adapter.dbt_adapter.conftest import dbt_test_helper
4
+ from tests.adapter.dbt_adapter.conftest import dbt_test_helper # noqa: F401
@@ -1,6 +1,10 @@
1
1
  import pytest
2
2
 
3
- from recce.tasks.histogram import HistogramDiffTask, HistogramDiffCheckValidator, _is_histogram_supported
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['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
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['current']['counts']) == 0
69
- assert run_result['current']['total'] == 0
70
- assert run_result['min'] is None
71
- assert run_result['max'] is None
72
- assert len(run_result['bin_edges']) == 0
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['base']['counts'][0] == 1
83
- assert run_result['base']['counts'][-1] == 1
84
- assert run_result['base']['total'] == 4
85
- assert run_result['current']['counts'][0] == 0
86
- assert run_result['current']['counts'][-1] == 0
87
- assert run_result['current']['total'] == 0
88
- assert run_result['min'] == 25
89
- assert run_result['max'] == 50
90
- assert run_result['bin_edges'][0] == 25
91
- assert run_result['bin_edges'][-1] == 51
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['base']['counts'][0] == 0
102
- assert run_result['base']['counts'][-1] == 0
103
- assert run_result['base']['total'] == 0
104
- assert run_result['current']['counts'][0] == 1
105
- assert run_result['current']['counts'][-1] == 1
106
- assert run_result['current']['total'] == 4
107
- assert run_result['min'] == 25
108
- assert run_result['max'] == 50
109
- assert run_result['bin_edges'][0] == 25
110
- assert run_result['bin_edges'][-1] == 51
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
- 'name': 'test',
117
- 'type': 'histogram_diff',
118
- 'params': params,
119
- 'view_options': view_options,
120
- })
121
-
122
- validate({
123
- "model": "customers",
124
- "column_name": "age",
125
- "column_type": "int",
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({})
@@ -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
- 'name': 'test',
11
- 'type': 'schema_diff',
12
- 'params': params,
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
- 'select': 'customers',
21
- 'exclude': 'customers',
22
- 'packages': ['jaffle_shop'],
23
- 'view_mode': 'all',
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
- 'packages': 'jaffle_shop',
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
- 'view_mode': None,
35
- })
36
- validate({
37
- 'view_mode': 'all',
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
- 'view_mode': 'abc',
42
- })
51
+ validate(
52
+ {
53
+ "view_mode": "abc",
54
+ }
55
+ )