recce-nightly 1.9.0.20250623__py3-none-any.whl → 1.25.0.20251112a2066__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.
- recce/VERSION +1 -1
- recce/__init__.py +5 -0
- recce/adapter/dbt_adapter/__init__.py +318 -240
- recce/artifact.py +76 -3
- recce/cli.py +703 -71
- recce/config.py +3 -3
- recce/connect_to_cloud.py +138 -0
- recce/core.py +3 -3
- recce/data/404.html +1 -22
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +12 -0
- recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_buildManifest.js +11 -0
- recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
- recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
- recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
- recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
- recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
- recce/data/_next/static/chunks/b2610ba997ff8c4f.js +110 -0
- recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
- recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
- recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
- recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
- recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
- recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
- recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.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.bd5c9f50.woff → 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-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → 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.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
- recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_not-found/__next._full.txt +17 -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 +10 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -8
- recce/event/__init__.py +9 -8
- recce/event/collector.py +6 -2
- recce/event/track.py +10 -0
- recce/github.py +1 -1
- recce/mcp_server.py +632 -0
- recce/models/types.py +23 -2
- recce/pull_request.py +1 -1
- recce/run.py +23 -16
- recce/server.py +194 -19
- 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 +2 -1
- recce/tasks/dataframe.py +59 -2
- recce/tasks/rowcount.py +4 -1
- recce/tasks/schema.py +4 -1
- recce/tasks/valuediff.py +1 -1
- recce/util/api_token.py +11 -2
- recce/util/breaking.py +9 -0
- recce/util/cll.py +1 -2
- recce/util/io.py +2 -2
- recce/util/lineage.py +19 -18
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +229 -5
- recce/yaml/__init__.py +2 -2
- recce_cloud/__init__.py +15 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +104 -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 +72 -0
- recce_cloud/api/gitlab.py +78 -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 +303 -0
- recce_cloud/upload.py +213 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/METADATA +31 -27
- recce_nightly-1.25.0.20251112a2066.dist-info/RECORD +178 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/test_dbt_cll.py +412 -79
- 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 +279 -0
- tests/test_cli.py +106 -3
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +148 -3
- tests/test_mcp_server.py +332 -0
- tests/test_server.py +6 -6
- tests/test_summary.py +14 -6
- recce/data/_next/static/WrRUb3nV8BhAZG_R8kVma/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
- recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
- recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
- recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
- recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
- recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
- recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
- recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
- recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
- recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
- recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
- recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
- recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
- recce/data/_next/static/chunks/92-7ab55ae02606193c.js +0 -1
- recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
- recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
- recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
- recce/data/_next/static/chunks/app/page-59241c42b7dd4fcf.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
- recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
- recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
- recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
- recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
- recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
- recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
- recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
- recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
- recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
- recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
- recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
- recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +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-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -785
- recce_nightly-1.9.0.20250623.dist-info/RECORD +0 -151
- tests/test_state.py +0 -134
- /recce/data/_next/static/{WrRUb3nV8BhAZG_R8kVma → 6LypcDXgyuSaiSCrsmUub}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
- /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/WHEEL +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions for Recce Cloud API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RecceCloudException(Exception):
|
|
9
|
+
"""Exception raised when Recce Cloud API returns an error."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, reason: str, status_code: int = None):
|
|
12
|
+
"""
|
|
13
|
+
Initialize exception.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
reason: Error reason/message
|
|
17
|
+
status_code: HTTP status code (optional)
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
reason = json.loads(reason).get("detail", reason)
|
|
21
|
+
except (json.JSONDecodeError, AttributeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
super().__init__(reason)
|
|
25
|
+
self.reason = reason
|
|
26
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory for creating platform-specific API clients.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from recce_cloud.api.github import GitHubRecceCloudClient
|
|
9
|
+
from recce_cloud.api.gitlab import GitLabRecceCloudClient
|
|
10
|
+
from recce_cloud.ci_providers import CIDetector
|
|
11
|
+
from recce_cloud.ci_providers.base import CIInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_platform_client(
|
|
15
|
+
token: str,
|
|
16
|
+
ci_info: Optional[CIInfo] = None,
|
|
17
|
+
api_host: Optional[str] = None,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Create a platform-specific Recce Cloud API client based on CI environment.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
token: Authentication token (GITHUB_TOKEN, CI_JOB_TOKEN, or RECCE_API_TOKEN)
|
|
24
|
+
ci_info: CI information (auto-detected if not provided)
|
|
25
|
+
api_host: Recce Cloud API host (optional)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
GitHubRecceCloudClient or GitLabRecceCloudClient
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If platform is not supported or required information is missing
|
|
32
|
+
"""
|
|
33
|
+
# Auto-detect CI info if not provided
|
|
34
|
+
if ci_info is None:
|
|
35
|
+
ci_info = CIDetector.detect()
|
|
36
|
+
|
|
37
|
+
if ci_info.platform == "github-actions":
|
|
38
|
+
repository = ci_info.repository or os.getenv("GITHUB_REPOSITORY")
|
|
39
|
+
if not repository:
|
|
40
|
+
raise ValueError("GitHub repository information is required but not detected")
|
|
41
|
+
|
|
42
|
+
return GitHubRecceCloudClient(token=token, repository=repository, api_host=api_host)
|
|
43
|
+
|
|
44
|
+
elif ci_info.platform == "gitlab-ci":
|
|
45
|
+
project_path = ci_info.repository or os.getenv("CI_PROJECT_PATH")
|
|
46
|
+
repository_url = os.getenv("CI_PROJECT_URL")
|
|
47
|
+
|
|
48
|
+
if not project_path:
|
|
49
|
+
raise ValueError("GitLab project path is required but not detected")
|
|
50
|
+
if not repository_url:
|
|
51
|
+
raise ValueError("GitLab project URL is required but not detected")
|
|
52
|
+
|
|
53
|
+
return GitLabRecceCloudClient(
|
|
54
|
+
token=token,
|
|
55
|
+
project_path=project_path,
|
|
56
|
+
repository_url=repository_url,
|
|
57
|
+
api_host=api_host,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unsupported platform: {ci_info.platform}. " "Only GitHub Actions and GitLab CI are supported."
|
|
63
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub-specific API client for Recce Cloud.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
from recce_cloud.api.base import BaseRecceCloudClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GitHubRecceCloudClient(BaseRecceCloudClient):
|
|
11
|
+
"""GitHub Actions-specific implementation of Recce Cloud API client."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, token: str, repository: str, api_host: Optional[str] = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize GitHub API client.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
token: GitHub token (GITHUB_TOKEN or RECCE_API_TOKEN)
|
|
19
|
+
repository: Repository in format "owner/repo"
|
|
20
|
+
api_host: Recce Cloud API host
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(token, api_host)
|
|
23
|
+
self.repository = repository
|
|
24
|
+
|
|
25
|
+
def touch_recce_session(
|
|
26
|
+
self,
|
|
27
|
+
branch: str,
|
|
28
|
+
adapter_type: str,
|
|
29
|
+
cr_number: Optional[int] = None,
|
|
30
|
+
) -> Dict:
|
|
31
|
+
"""
|
|
32
|
+
Create or touch a Recce session for GitHub Actions.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
branch: Branch name
|
|
36
|
+
adapter_type: DBT adapter type
|
|
37
|
+
cr_number: PR number for pull request sessions
|
|
38
|
+
commit_sha: Not used for GitHub (optional for compatibility)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dictionary containing session_id, manifest_upload_url, catalog_upload_url
|
|
42
|
+
"""
|
|
43
|
+
url = f"{self.api_host}/api/v2/github/{self.repository}/touch-recce-session"
|
|
44
|
+
|
|
45
|
+
payload = {
|
|
46
|
+
"branch": branch,
|
|
47
|
+
"adapter_type": adapter_type,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if cr_number is not None:
|
|
51
|
+
payload["pr_number"] = cr_number
|
|
52
|
+
|
|
53
|
+
return self._make_request("POST", url, json=payload)
|
|
54
|
+
|
|
55
|
+
def upload_completed(self, session_id: str, commit_sha: Optional[str] = None) -> Dict:
|
|
56
|
+
"""
|
|
57
|
+
Notify Recce Cloud that upload is complete for GitHub.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
session_id: Session ID from touch_recce_session
|
|
61
|
+
commit_sha: Not used for GitHub (optional for compatibility)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Empty dictionary or acknowledgement
|
|
65
|
+
"""
|
|
66
|
+
url = f"{self.api_host}/api/v2/github/{self.repository}/upload-completed"
|
|
67
|
+
|
|
68
|
+
payload = {
|
|
69
|
+
"session_id": session_id,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return self._make_request("POST", url, json=payload)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitLab-specific API client for Recce Cloud.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
from recce_cloud.api.base import BaseRecceCloudClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GitLabRecceCloudClient(BaseRecceCloudClient):
|
|
11
|
+
"""GitLab CI-specific implementation of Recce Cloud API client."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, token: str, project_path: str, repository_url: str, api_host: Optional[str] = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize GitLab API client.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
token: GitLab token (CI_JOB_TOKEN or RECCE_API_TOKEN)
|
|
19
|
+
project_path: Project path in format "group/project"
|
|
20
|
+
repository_url: Full repository URL (e.g., https://gitlab.com/group/project)
|
|
21
|
+
api_host: Recce Cloud API host
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(token, api_host)
|
|
24
|
+
self.project_path = project_path
|
|
25
|
+
self.repository_url = repository_url
|
|
26
|
+
|
|
27
|
+
def touch_recce_session(
|
|
28
|
+
self,
|
|
29
|
+
branch: str,
|
|
30
|
+
adapter_type: str,
|
|
31
|
+
cr_number: Optional[int] = None,
|
|
32
|
+
commit_sha: Optional[str] = None,
|
|
33
|
+
) -> Dict:
|
|
34
|
+
"""
|
|
35
|
+
Create or touch a Recce session for GitLab CI.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
branch: Branch name
|
|
39
|
+
adapter_type: DBT adapter type
|
|
40
|
+
cr_number: MR IID for merge request sessions
|
|
41
|
+
commit_sha: Commit SHA (required for GitLab)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary containing session_id, manifest_upload_url, catalog_upload_url
|
|
45
|
+
"""
|
|
46
|
+
url = f"{self.api_host}/api/v2/gitlab/{self.project_path}/touch-recce-session"
|
|
47
|
+
|
|
48
|
+
payload = {
|
|
49
|
+
"branch": branch,
|
|
50
|
+
"adapter_type": adapter_type,
|
|
51
|
+
"commit_sha": commit_sha,
|
|
52
|
+
"repository_url": self.repository_url,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if cr_number is not None:
|
|
56
|
+
payload["mr_iid"] = cr_number
|
|
57
|
+
|
|
58
|
+
return self._make_request("POST", url, json=payload)
|
|
59
|
+
|
|
60
|
+
def upload_completed(self, session_id: str, commit_sha: Optional[str] = None) -> Dict:
|
|
61
|
+
"""
|
|
62
|
+
Notify Recce Cloud that upload is complete for GitLab.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
session_id: Session ID from touch_recce_session
|
|
66
|
+
commit_sha: Commit SHA (required for GitLab)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Empty dictionary or acknowledgement
|
|
70
|
+
"""
|
|
71
|
+
url = f"{self.api_host}/api/v2/gitlab/{self.project_path}/upload-completed"
|
|
72
|
+
|
|
73
|
+
payload = {
|
|
74
|
+
"session_id": session_id,
|
|
75
|
+
"commit_sha": commit_sha,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return self._make_request("POST", url, json=payload)
|
recce_cloud/artifact.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Artifact utilities for recce-cloud.
|
|
3
|
+
|
|
4
|
+
Simplified version of recce.artifact with only the functions needed
|
|
5
|
+
for upload-session functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_artifacts_path(target_path: str) -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Verify if the target path contains valid dbt artifacts.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
target_path: Path to the directory containing artifacts
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if the target path contains manifest.json and catalog.json
|
|
21
|
+
"""
|
|
22
|
+
if not target_path:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
if not os.path.exists(target_path):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
if not os.path.isdir(target_path):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
required_artifacts_files = ["manifest.json", "catalog.json"]
|
|
32
|
+
|
|
33
|
+
if all(f in os.listdir(target_path) for f in required_artifacts_files):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_adapter_type(manifest_path: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Extract adapter type from manifest.json.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
manifest_path: Path to manifest.json file
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Adapter type string (e.g., "postgres", "snowflake", "bigquery")
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
Exception: If adapter type cannot be found in manifest
|
|
51
|
+
"""
|
|
52
|
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
53
|
+
manifest_data = json.load(f)
|
|
54
|
+
adapter_type = manifest_data.get("metadata", {}).get("adapter_type")
|
|
55
|
+
if adapter_type is None:
|
|
56
|
+
raise Exception("Failed to parse adapter type from manifest.json")
|
|
57
|
+
return adapter_type
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CI/CD Provider detection and information extraction.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from recce_cloud.ci_providers.detector import CIDetector
|
|
6
|
+
from recce_cloud.ci_providers.github_actions import GitHubActionsProvider
|
|
7
|
+
from recce_cloud.ci_providers.gitlab_ci import GitLabCIProvider
|
|
8
|
+
|
|
9
|
+
__all__ = ["CIDetector", "GitHubActionsProvider", "GitLabCIProvider"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base CI provider interface.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CIInfo:
|
|
13
|
+
"""Information extracted from CI environment."""
|
|
14
|
+
|
|
15
|
+
platform: Optional[str] = None # "github-actions", "gitlab-ci", etc.
|
|
16
|
+
cr_number: Optional[int] = None # Change request number (PR/MR)
|
|
17
|
+
cr_url: Optional[str] = None # Change request URL (for session linking)
|
|
18
|
+
session_type: Optional[str] = None # "cr", "prod", "dev"
|
|
19
|
+
commit_sha: Optional[str] = None # Full commit SHA
|
|
20
|
+
base_branch: Optional[str] = None # Target/base branch
|
|
21
|
+
source_branch: Optional[str] = None # Source/head branch
|
|
22
|
+
repository: Optional[str] = None # Repository path (owner/repo or group/project)
|
|
23
|
+
access_token: Optional[str] = None # CI-provided access token (GITHUB_TOKEN, CI_JOB_TOKEN)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseCIProvider(ABC):
|
|
27
|
+
"""Abstract base class for CI provider detection and info extraction."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def can_handle(self) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Check if this provider can handle the current environment.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if the provider's CI platform is detected
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def extract_ci_info(self) -> CIInfo:
|
|
41
|
+
"""
|
|
42
|
+
Extract CI information from environment variables.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
CIInfo object with extracted information
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def run_git_command(command: list[str]) -> Optional[str]:
|
|
51
|
+
"""
|
|
52
|
+
Run a git command and return output.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
command: Git command as list (e.g., ['git', 'rev-parse', 'HEAD'])
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Command output stripped of whitespace, or None if command fails
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=5)
|
|
62
|
+
return result.stdout.strip()
|
|
63
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def determine_session_type(cr_number: Optional[int], source_branch: Optional[str]) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Determine session type based on context.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
cr_number: Change request number (PR/MR)
|
|
73
|
+
source_branch: Source branch name
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Session type: "cr", "prod", or "dev"
|
|
77
|
+
"""
|
|
78
|
+
if cr_number is not None:
|
|
79
|
+
return "cr"
|
|
80
|
+
if source_branch in ["main", "master"]:
|
|
81
|
+
return "prod"
|
|
82
|
+
return "dev"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CI provider detection and orchestration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from recce_cloud.ci_providers.base import BaseCIProvider, CIInfo
|
|
10
|
+
from recce_cloud.ci_providers.github_actions import GitHubActionsProvider
|
|
11
|
+
from recce_cloud.ci_providers.gitlab_ci import GitLabCIProvider
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CIDetector:
|
|
17
|
+
"""
|
|
18
|
+
Detects CI platform and extracts information.
|
|
19
|
+
|
|
20
|
+
Supports:
|
|
21
|
+
- GitHub Actions
|
|
22
|
+
- GitLab CI/CD
|
|
23
|
+
- Generic fallback (git commands)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Order matters: check in priority order
|
|
27
|
+
PROVIDERS = [
|
|
28
|
+
GitHubActionsProvider,
|
|
29
|
+
GitLabCIProvider,
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def detect(cls) -> CIInfo:
|
|
34
|
+
"""
|
|
35
|
+
Detect CI platform and extract information.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
CIInfo object with detected information
|
|
39
|
+
"""
|
|
40
|
+
# Try each provider in order
|
|
41
|
+
for provider_class in cls.PROVIDERS:
|
|
42
|
+
provider = provider_class()
|
|
43
|
+
if provider.can_handle():
|
|
44
|
+
logger.info(f"CI Platform: {provider_class.__name__.replace('Provider', '')}")
|
|
45
|
+
ci_info = provider.extract_ci_info()
|
|
46
|
+
cls._log_detected_values(ci_info)
|
|
47
|
+
return ci_info
|
|
48
|
+
|
|
49
|
+
# No CI platform detected, use generic fallback
|
|
50
|
+
logger.info("No CI platform detected, using git fallback")
|
|
51
|
+
ci_info = cls._fallback_detection()
|
|
52
|
+
cls._log_detected_values(ci_info)
|
|
53
|
+
return ci_info
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def apply_overrides(
|
|
57
|
+
cls,
|
|
58
|
+
ci_info: CIInfo,
|
|
59
|
+
cr: Optional[int] = None,
|
|
60
|
+
session_type: Optional[str] = None,
|
|
61
|
+
) -> CIInfo:
|
|
62
|
+
"""
|
|
63
|
+
Apply manual overrides to detected CI information.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
ci_info: Detected CI information
|
|
67
|
+
cr: Manual change request number override
|
|
68
|
+
session_type: Manual session type override
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
CIInfo with overrides applied
|
|
72
|
+
"""
|
|
73
|
+
# Log overrides
|
|
74
|
+
if cr is not None and cr != ci_info.cr_number:
|
|
75
|
+
logger.info(f"Using manual override: --cr {cr} (detected: {ci_info.cr_number})")
|
|
76
|
+
ci_info.cr_number = cr
|
|
77
|
+
# Rebuild CR URL if we have repository info
|
|
78
|
+
if ci_info.repository:
|
|
79
|
+
if ci_info.platform == "github-actions":
|
|
80
|
+
ci_info.cr_url = f"https://github.com/{ci_info.repository}/pull/{cr}"
|
|
81
|
+
elif ci_info.platform == "gitlab-ci":
|
|
82
|
+
server_url = os.getenv("CI_SERVER_URL", "https://gitlab.com")
|
|
83
|
+
ci_info.cr_url = f"{server_url}/{ci_info.repository}/-/merge_requests/{cr}"
|
|
84
|
+
|
|
85
|
+
if session_type is not None and session_type != ci_info.session_type:
|
|
86
|
+
logger.info(f"Using manual override: --type {session_type} (detected: {ci_info.session_type})")
|
|
87
|
+
ci_info.session_type = session_type
|
|
88
|
+
|
|
89
|
+
# Re-determine session type if CR was overridden
|
|
90
|
+
if cr is not None:
|
|
91
|
+
if session_type is None: # Only if not manually overridden
|
|
92
|
+
ci_info.session_type = BaseCIProvider.determine_session_type(ci_info.cr_number, ci_info.source_branch)
|
|
93
|
+
|
|
94
|
+
return ci_info
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def _fallback_detection(cls) -> CIInfo:
|
|
98
|
+
"""
|
|
99
|
+
Fallback detection using git commands when no CI platform is detected.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
CIInfo with basic git information
|
|
103
|
+
"""
|
|
104
|
+
commit_sha = BaseCIProvider.run_git_command(["git", "rev-parse", "HEAD"])
|
|
105
|
+
source_branch = BaseCIProvider.run_git_command(["git", "branch", "--show-current"])
|
|
106
|
+
|
|
107
|
+
session_type = BaseCIProvider.determine_session_type(None, source_branch)
|
|
108
|
+
|
|
109
|
+
return CIInfo(
|
|
110
|
+
platform=None,
|
|
111
|
+
cr_number=None,
|
|
112
|
+
session_type=session_type,
|
|
113
|
+
commit_sha=commit_sha,
|
|
114
|
+
base_branch="main", # Default
|
|
115
|
+
source_branch=source_branch,
|
|
116
|
+
repository=None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def _log_detected_values(cls, ci_info: CIInfo) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Log detected values for transparency.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
ci_info: Detected CI information
|
|
126
|
+
"""
|
|
127
|
+
if ci_info.cr_number is not None:
|
|
128
|
+
if ci_info.platform == "github-actions":
|
|
129
|
+
logger.info(f"Detected PR number: {ci_info.cr_number}")
|
|
130
|
+
elif ci_info.platform == "gitlab-ci":
|
|
131
|
+
logger.info(f"Detected MR number: {ci_info.cr_number}")
|
|
132
|
+
else:
|
|
133
|
+
logger.info(f"Detected CR number: {ci_info.cr_number}")
|
|
134
|
+
else:
|
|
135
|
+
logger.info("No CR number detected")
|
|
136
|
+
|
|
137
|
+
if ci_info.commit_sha:
|
|
138
|
+
logger.info(f"Detected commit SHA: {ci_info.commit_sha[:8]}...")
|
|
139
|
+
else:
|
|
140
|
+
logger.warning("Could not detect commit SHA")
|
|
141
|
+
|
|
142
|
+
logger.info(f"Detected base branch: {ci_info.base_branch}")
|
|
143
|
+
logger.info(f"Detected source branch: {ci_info.source_branch}")
|
|
144
|
+
logger.info(f"Session type: {ci_info.session_type}")
|
|
145
|
+
|
|
146
|
+
if ci_info.repository:
|
|
147
|
+
logger.info(f"Repository: {ci_info.repository}")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Actions CI provider.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from recce_cloud.ci_providers.base import BaseCIProvider, CIInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitHubActionsProvider(BaseCIProvider):
|
|
13
|
+
"""GitHub Actions CI provider implementation."""
|
|
14
|
+
|
|
15
|
+
def can_handle(self) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Check if running in GitHub Actions.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if GITHUB_ACTIONS environment variable is 'true'
|
|
21
|
+
"""
|
|
22
|
+
return os.getenv("GITHUB_ACTIONS") == "true"
|
|
23
|
+
|
|
24
|
+
def extract_ci_info(self) -> CIInfo:
|
|
25
|
+
"""
|
|
26
|
+
Extract CI information from GitHub Actions environment.
|
|
27
|
+
|
|
28
|
+
Environment variables used:
|
|
29
|
+
- GITHUB_EVENT_PATH: Path to event payload JSON (for PR number)
|
|
30
|
+
- GITHUB_SHA: Commit SHA
|
|
31
|
+
- GITHUB_BASE_REF: Base/target branch (PR only)
|
|
32
|
+
- GITHUB_HEAD_REF: Source/head branch (PR only)
|
|
33
|
+
- GITHUB_REF_NAME: Branch name (fallback)
|
|
34
|
+
- GITHUB_REPOSITORY: Repository (owner/repo)
|
|
35
|
+
- GITHUB_TOKEN: Default access token (automatically provided by GitHub Actions)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
CIInfo object with extracted information
|
|
39
|
+
"""
|
|
40
|
+
cr_number = self._extract_pr_number()
|
|
41
|
+
commit_sha = self._extract_commit_sha()
|
|
42
|
+
base_branch = self._extract_base_branch()
|
|
43
|
+
source_branch = self._extract_source_branch()
|
|
44
|
+
repository = os.getenv("GITHUB_REPOSITORY")
|
|
45
|
+
access_token = os.getenv("GITHUB_TOKEN")
|
|
46
|
+
|
|
47
|
+
# Build CR URL (PR URL) if we have the necessary information
|
|
48
|
+
cr_url = None
|
|
49
|
+
if cr_number is not None and repository:
|
|
50
|
+
cr_url = f"https://github.com/{repository}/pull/{cr_number}"
|
|
51
|
+
|
|
52
|
+
session_type = self.determine_session_type(cr_number, source_branch)
|
|
53
|
+
|
|
54
|
+
return CIInfo(
|
|
55
|
+
platform="github-actions",
|
|
56
|
+
cr_number=cr_number,
|
|
57
|
+
cr_url=cr_url,
|
|
58
|
+
session_type=session_type,
|
|
59
|
+
commit_sha=commit_sha,
|
|
60
|
+
base_branch=base_branch,
|
|
61
|
+
source_branch=source_branch,
|
|
62
|
+
repository=repository,
|
|
63
|
+
access_token=access_token,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _extract_pr_number(self) -> Optional[int]:
|
|
67
|
+
"""
|
|
68
|
+
Extract PR number from GitHub event payload.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
PR number if detected, None otherwise
|
|
72
|
+
"""
|
|
73
|
+
event_path = os.getenv("GITHUB_EVENT_PATH")
|
|
74
|
+
if not event_path or not os.path.exists(event_path):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
with open(event_path, "r") as f:
|
|
79
|
+
event_data = json.load(f)
|
|
80
|
+
pr_data = event_data.get("pull_request", {})
|
|
81
|
+
pr_number = pr_data.get("number")
|
|
82
|
+
if pr_number is not None:
|
|
83
|
+
return int(pr_number)
|
|
84
|
+
except (json.JSONDecodeError, ValueError, OSError):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def _extract_commit_sha(self) -> Optional[str]:
|
|
90
|
+
"""
|
|
91
|
+
Extract commit SHA from GitHub Actions environment.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Commit SHA if detected, falls back to git command
|
|
95
|
+
"""
|
|
96
|
+
commit_sha = os.getenv("GITHUB_SHA")
|
|
97
|
+
if commit_sha:
|
|
98
|
+
return commit_sha
|
|
99
|
+
|
|
100
|
+
# Fallback to git command
|
|
101
|
+
return self.run_git_command(["git", "rev-parse", "HEAD"])
|
|
102
|
+
|
|
103
|
+
def _extract_base_branch(self) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Extract base/target branch from GitHub Actions environment.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Base branch name, defaults to 'main' if not detected
|
|
109
|
+
"""
|
|
110
|
+
# GITHUB_BASE_REF is only set for pull_request events
|
|
111
|
+
base_branch = os.getenv("GITHUB_BASE_REF")
|
|
112
|
+
if base_branch:
|
|
113
|
+
return base_branch
|
|
114
|
+
|
|
115
|
+
# Default to main
|
|
116
|
+
return "main"
|
|
117
|
+
|
|
118
|
+
def _extract_source_branch(self) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
Extract source/head branch from GitHub Actions environment.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Source branch name if detected
|
|
124
|
+
"""
|
|
125
|
+
# GITHUB_HEAD_REF is only set for pull_request events
|
|
126
|
+
source_branch = os.getenv("GITHUB_HEAD_REF")
|
|
127
|
+
if source_branch:
|
|
128
|
+
return source_branch
|
|
129
|
+
|
|
130
|
+
# Fallback to GITHUB_REF_NAME
|
|
131
|
+
source_branch = os.getenv("GITHUB_REF_NAME")
|
|
132
|
+
if source_branch:
|
|
133
|
+
return source_branch
|
|
134
|
+
|
|
135
|
+
# Fallback to git command
|
|
136
|
+
return self.run_git_command(["git", "branch", "--show-current"])
|