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.
Files changed (169) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +5 -0
  3. recce/adapter/dbt_adapter/__init__.py +318 -240
  4. recce/artifact.py +76 -3
  5. recce/cli.py +703 -71
  6. recce/config.py +3 -3
  7. recce/connect_to_cloud.py +138 -0
  8. recce/core.py +3 -3
  9. recce/data/404.html +1 -22
  10. recce/data/__next.__PAGE__.txt +10 -0
  11. recce/data/__next._full.txt +23 -0
  12. recce/data/__next._index.txt +8 -0
  13. recce/data/__next._tree.txt +12 -0
  14. recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_buildManifest.js +11 -0
  15. recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_clientMiddlewareManifest.json +1 -0
  16. recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
  17. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  18. recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
  19. recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
  20. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  21. recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
  22. recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
  23. recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
  24. recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
  25. recce/data/_next/static/chunks/b2610ba997ff8c4f.js +110 -0
  26. recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
  27. recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
  28. recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
  29. recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
  30. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  31. recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
  32. recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
  33. recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
  34. recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.js +3 -0
  35. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  36. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  37. recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
  38. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  39. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  40. recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
  41. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  42. recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
  43. recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
  44. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  45. recce/data/_not-found/__next._full.txt +17 -0
  46. recce/data/_not-found/__next._index.txt +8 -0
  47. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  48. recce/data/_not-found/__next._not-found.txt +4 -0
  49. recce/data/_not-found/__next._tree.txt +10 -0
  50. recce/data/_not-found.html +1 -0
  51. recce/data/_not-found.txt +17 -0
  52. recce/data/auth_callback.html +68 -0
  53. recce/data/index.html +1 -27
  54. recce/data/index.txt +23 -8
  55. recce/event/__init__.py +9 -8
  56. recce/event/collector.py +6 -2
  57. recce/event/track.py +10 -0
  58. recce/github.py +1 -1
  59. recce/mcp_server.py +632 -0
  60. recce/models/types.py +23 -2
  61. recce/pull_request.py +1 -1
  62. recce/run.py +23 -16
  63. recce/server.py +194 -19
  64. recce/state/__init__.py +31 -0
  65. recce/state/cloud.py +632 -0
  66. recce/state/const.py +26 -0
  67. recce/state/local.py +56 -0
  68. recce/state/state.py +119 -0
  69. recce/state/state_loader.py +174 -0
  70. recce/summary.py +2 -1
  71. recce/tasks/dataframe.py +59 -2
  72. recce/tasks/rowcount.py +4 -1
  73. recce/tasks/schema.py +4 -1
  74. recce/tasks/valuediff.py +1 -1
  75. recce/util/api_token.py +11 -2
  76. recce/util/breaking.py +9 -0
  77. recce/util/cll.py +1 -2
  78. recce/util/io.py +2 -2
  79. recce/util/lineage.py +19 -18
  80. recce/util/perf_tracking.py +85 -0
  81. recce/util/recce_cloud.py +229 -5
  82. recce/yaml/__init__.py +2 -2
  83. recce_cloud/__init__.py +15 -0
  84. recce_cloud/api/__init__.py +17 -0
  85. recce_cloud/api/base.py +104 -0
  86. recce_cloud/api/client.py +150 -0
  87. recce_cloud/api/exceptions.py +26 -0
  88. recce_cloud/api/factory.py +63 -0
  89. recce_cloud/api/github.py +72 -0
  90. recce_cloud/api/gitlab.py +78 -0
  91. recce_cloud/artifact.py +57 -0
  92. recce_cloud/ci_providers/__init__.py +9 -0
  93. recce_cloud/ci_providers/base.py +82 -0
  94. recce_cloud/ci_providers/detector.py +147 -0
  95. recce_cloud/ci_providers/github_actions.py +136 -0
  96. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  97. recce_cloud/cli.py +303 -0
  98. recce_cloud/upload.py +213 -0
  99. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/METADATA +31 -27
  100. recce_nightly-1.25.0.20251112a2066.dist-info/RECORD +178 -0
  101. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/top_level.txt +1 -0
  102. tests/adapter/dbt_adapter/test_dbt_cll.py +412 -79
  103. tests/recce_cloud/__init__.py +0 -0
  104. tests/recce_cloud/test_ci_providers.py +351 -0
  105. tests/recce_cloud/test_cli.py +372 -0
  106. tests/recce_cloud/test_client.py +273 -0
  107. tests/recce_cloud/test_platform_clients.py +279 -0
  108. tests/test_cli.py +106 -3
  109. tests/test_cli_mcp_optional.py +45 -0
  110. tests/test_cloud_listing_cli.py +324 -0
  111. tests/test_connect_to_cloud.py +82 -0
  112. tests/test_core.py +148 -3
  113. tests/test_mcp_server.py +332 -0
  114. tests/test_server.py +6 -6
  115. tests/test_summary.py +14 -6
  116. recce/data/_next/static/WrRUb3nV8BhAZG_R8kVma/_buildManifest.js +0 -1
  117. recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
  118. recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
  119. recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
  120. recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
  121. recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
  122. recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
  123. recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
  124. recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
  125. recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
  126. recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
  127. recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
  128. recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
  129. recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
  130. recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
  131. recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
  132. recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
  133. recce/data/_next/static/chunks/92-7ab55ae02606193c.js +0 -1
  134. recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
  135. recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
  136. recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
  137. recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
  138. recce/data/_next/static/chunks/app/page-59241c42b7dd4fcf.js +0 -1
  139. recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
  140. recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
  141. recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
  142. recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
  143. recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
  144. recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
  145. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  146. recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
  147. recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
  148. recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
  149. recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
  150. recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
  151. recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
  152. recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
  153. recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
  154. recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
  155. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  156. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  157. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  158. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  159. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  160. recce/state.py +0 -785
  161. recce_nightly-1.9.0.20250623.dist-info/RECORD +0 -151
  162. tests/test_state.py +0 -134
  163. /recce/data/_next/static/{WrRUb3nV8BhAZG_R8kVma → 6LypcDXgyuSaiSCrsmUub}/_ssgManifest.js +0 -0
  164. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  165. /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
  166. /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
  167. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/WHEEL +0 -0
  168. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/entry_points.txt +0 -0
  169. {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,130 @@
1
+ """
2
+ GitLab CI/CD provider.
3
+ """
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ from recce_cloud.ci_providers.base import BaseCIProvider, CIInfo
9
+
10
+
11
+ class GitLabCIProvider(BaseCIProvider):
12
+ """GitLab CI/CD provider implementation."""
13
+
14
+ def can_handle(self) -> bool:
15
+ """
16
+ Check if running in GitLab CI.
17
+
18
+ Returns:
19
+ True if GITLAB_CI environment variable is 'true'
20
+ """
21
+ return os.getenv("GITLAB_CI") == "true"
22
+
23
+ def extract_ci_info(self) -> CIInfo:
24
+ """
25
+ Extract CI information from GitLab CI environment.
26
+
27
+ Environment variables used:
28
+ - CI_MERGE_REQUEST_IID: Merge request number
29
+ - CI_COMMIT_SHA: Commit SHA
30
+ - CI_MERGE_REQUEST_TARGET_BRANCH_NAME: Target branch (MR only)
31
+ - CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: Source branch (MR only)
32
+ - CI_COMMIT_REF_NAME: Branch name (fallback)
33
+ - CI_PROJECT_PATH: Repository path (group/project)
34
+ - CI_SERVER_URL: GitLab instance URL (defaults to https://gitlab.com)
35
+ - CI_JOB_TOKEN: Default access token (automatically provided by GitLab CI)
36
+
37
+ Returns:
38
+ CIInfo object with extracted information
39
+ """
40
+ cr_number = self._extract_mr_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("CI_PROJECT_PATH")
45
+ access_token = os.getenv("CI_JOB_TOKEN")
46
+
47
+ # Build CR URL (MR URL) if we have the necessary information
48
+ cr_url = None
49
+ if cr_number is not None and repository:
50
+ server_url = os.getenv("CI_SERVER_URL", "https://gitlab.com")
51
+ cr_url = f"{server_url}/{repository}/-/merge_requests/{cr_number}"
52
+
53
+ session_type = self.determine_session_type(cr_number, source_branch)
54
+
55
+ return CIInfo(
56
+ platform="gitlab-ci",
57
+ cr_number=cr_number,
58
+ cr_url=cr_url,
59
+ session_type=session_type,
60
+ commit_sha=commit_sha,
61
+ base_branch=base_branch,
62
+ source_branch=source_branch,
63
+ repository=repository,
64
+ access_token=access_token,
65
+ )
66
+
67
+ def _extract_mr_number(self) -> Optional[int]:
68
+ """
69
+ Extract MR number from GitLab CI environment.
70
+
71
+ Returns:
72
+ MR number if detected, None otherwise
73
+ """
74
+ mr_iid = os.getenv("CI_MERGE_REQUEST_IID")
75
+ if mr_iid:
76
+ try:
77
+ return int(mr_iid)
78
+ except ValueError:
79
+ pass
80
+
81
+ return None
82
+
83
+ def _extract_commit_sha(self) -> Optional[str]:
84
+ """
85
+ Extract commit SHA from GitLab CI environment.
86
+
87
+ Returns:
88
+ Commit SHA if detected, falls back to git command
89
+ """
90
+ commit_sha = os.getenv("CI_COMMIT_SHA")
91
+ if commit_sha:
92
+ return commit_sha
93
+
94
+ # Fallback to git command
95
+ return self.run_git_command(["git", "rev-parse", "HEAD"])
96
+
97
+ def _extract_base_branch(self) -> str:
98
+ """
99
+ Extract base/target branch from GitLab CI environment.
100
+
101
+ Returns:
102
+ Base branch name, defaults to 'main' if not detected
103
+ """
104
+ # CI_MERGE_REQUEST_TARGET_BRANCH_NAME is only set for merge request pipelines
105
+ base_branch = os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")
106
+ if base_branch:
107
+ return base_branch
108
+
109
+ # Default to main
110
+ return "main"
111
+
112
+ def _extract_source_branch(self) -> Optional[str]:
113
+ """
114
+ Extract source branch from GitLab CI environment.
115
+
116
+ Returns:
117
+ Source branch name if detected
118
+ """
119
+ # CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is only set for merge request pipelines
120
+ source_branch = os.getenv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")
121
+ if source_branch:
122
+ return source_branch
123
+
124
+ # Fallback to CI_COMMIT_REF_NAME
125
+ source_branch = os.getenv("CI_COMMIT_REF_NAME")
126
+ if source_branch:
127
+ return source_branch
128
+
129
+ # Fallback to git command
130
+ return self.run_git_command(["git", "branch", "--show-current"])
recce_cloud/cli.py ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Recce Cloud CLI - Lightweight command for managing Recce Cloud operations.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ import sys
9
+
10
+ import click
11
+ from rich.console import Console
12
+ from rich.logging import RichHandler
13
+
14
+ from recce_cloud import __version__
15
+ from recce_cloud.artifact import get_adapter_type, verify_artifacts_path
16
+ from recce_cloud.ci_providers import CIDetector
17
+ from recce_cloud.upload import upload_to_existing_session, upload_with_platform_apis
18
+
19
+ # Configure logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format="%(message)s",
23
+ handlers=[RichHandler(console=Console(stderr=True), show_time=False, show_path=False)],
24
+ )
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @click.group()
29
+ def cloud_cli():
30
+ """
31
+ Recce Cloud CLI - Manage Recce Cloud sessions and state files.
32
+
33
+ A lightweight tool for CI/CD environments to interact with Recce Cloud
34
+ without the heavy dependencies of the full recce package.
35
+ """
36
+ pass
37
+
38
+
39
+ @cloud_cli.command()
40
+ def version():
41
+ """Show the version of recce-cloud."""
42
+ click.echo(__version__)
43
+
44
+
45
+ @cloud_cli.command()
46
+ @click.option(
47
+ "--target-path",
48
+ type=click.Path(exists=True),
49
+ default="target",
50
+ help="Path to dbt target directory containing manifest.json and catalog.json",
51
+ )
52
+ @click.option(
53
+ "--session-id",
54
+ envvar="RECCE_SESSION_ID",
55
+ help="Recce Cloud session ID to upload artifacts to (or use RECCE_SESSION_ID env var). "
56
+ "If not provided, session will be created automatically using platform-specific APIs (GitHub/GitLab).",
57
+ )
58
+ @click.option(
59
+ "--cr",
60
+ type=int,
61
+ help="Change request number (PR/MR) (overrides auto-detection)",
62
+ )
63
+ @click.option(
64
+ "--type",
65
+ "session_type",
66
+ type=click.Choice(["cr", "prod", "dev"]),
67
+ help="Session type (overrides auto-detection)",
68
+ )
69
+ @click.option(
70
+ "--dry-run",
71
+ is_flag=True,
72
+ help="Show what would be uploaded without actually uploading",
73
+ )
74
+ def upload(target_path, session_id, cr, session_type, dry_run):
75
+ """
76
+ Upload dbt artifacts to Recce Cloud session.
77
+
78
+ Lightweight replacement for 'recce upload-session' designed for CI/CD environments.
79
+ Supports two workflows: auto-session creation (GitHub/GitLab) or upload to existing session.
80
+
81
+ \b
82
+ What gets uploaded:
83
+ - manifest.json: dbt project structure and model definitions
84
+ - catalog.json: database catalog information and statistics
85
+
86
+ \b
87
+ Upload Workflows:
88
+
89
+ 1. Platform-Specific (Recommended for GitHub Actions & GitLab CI):
90
+ - Automatically creates session using platform APIs
91
+ - No --session-id required
92
+ - Detects PR/MR context and links session automatically
93
+ - Example: recce-cloud upload
94
+
95
+ 2. Generic (For existing sessions or other CI platforms):
96
+ - Upload to pre-existing session using session ID
97
+ - Requires --session-id parameter
98
+ - Example: recce-cloud upload --session-id abc123
99
+
100
+ \b
101
+ The upload process:
102
+ 1. Auto-detect CI/CD platform (GitHub Actions, GitLab CI, etc.)
103
+ 2. Validate dbt artifact files exist and are valid JSON
104
+ 3. Extract adapter type from manifest.json
105
+ 4. Authenticate with Recce Cloud (RECCE_API_TOKEN or CI token)
106
+ 5. Create/touch session (platform-specific) OR get session info (generic)
107
+ 6. Upload artifacts to S3 using presigned URLs
108
+ 7. Notify upload completion (platform-specific only)
109
+
110
+ \b
111
+ About Recce Cloud Sessions:
112
+ - Sessions compare base (production) and current (PR/MR) environments
113
+ - Each session stores manifests/catalogs from both environments
114
+ - Sessions are linked to PRs/MRs for team collaboration and review
115
+ - Platform-specific workflow automatically handles session creation
116
+
117
+ \b
118
+ Authentication Priority:
119
+ 1. RECCE_API_TOKEN environment variable (explicit token)
120
+ 2. GITHUB_TOKEN (GitHub Actions) or CI_JOB_TOKEN (GitLab CI)
121
+ 3. Error if no token available
122
+
123
+ \b
124
+ Auto-Detection:
125
+ This command automatically detects:
126
+ - CI/CD Platform: GitHub Actions, GitLab CI (others supported with --session-id)
127
+ - Repository: GitHub owner/repo or GitLab group/project
128
+ - Branch: Source branch and base branch
129
+ - Change Request: PR number (GitHub) or MR IID (GitLab)
130
+ - Commit: Current commit SHA
131
+
132
+ \b
133
+ Environment Variables:
134
+ - RECCE_SESSION_ID: Target session ID (optional, for generic workflow)
135
+ - RECCE_API_TOKEN: Recce Cloud API token (recommended)
136
+ - GITHUB_TOKEN: GitHub authentication (auto-detected)
137
+ - CI_JOB_TOKEN: GitLab authentication (auto-detected)
138
+
139
+ \b
140
+ Examples:
141
+
142
+ # Platform-specific workflow (GitHub Actions)
143
+ recce-cloud upload
144
+
145
+ # Platform-specific workflow (GitLab CI)
146
+ recce-cloud upload
147
+
148
+ # Generic workflow with session ID
149
+ export RECCE_SESSION_ID=abc123
150
+ recce-cloud upload
151
+
152
+ # Custom target path with manual overrides
153
+ recce-cloud upload --target-path my-target --cr 456 --type cr
154
+
155
+ \b
156
+ Exit Codes:
157
+ 0 - Success
158
+ 1 - Platform not supported (for platform-specific workflow)
159
+ 2 - Authentication error
160
+ 3 - File validation error (missing or invalid manifest/catalog)
161
+ 4 - Upload error
162
+ """
163
+ console = Console()
164
+
165
+ # 1. Auto-detect CI environment information
166
+ console.rule("Auto-detecting CI environment", style="blue")
167
+ try:
168
+ ci_info = CIDetector.detect()
169
+ ci_info = CIDetector.apply_overrides(ci_info, cr=cr, session_type=session_type)
170
+ except Exception as e:
171
+ console.print(f"[yellow]Warning:[/yellow] Failed to detect CI environment: {e}")
172
+ console.print("Continuing without CI metadata...")
173
+ ci_info = None
174
+
175
+ # 2. Validate artifacts exist
176
+ if not verify_artifacts_path(target_path):
177
+ console.print(f"[red]Error:[/red] Invalid target path: {target_path}")
178
+ console.print("Please provide a valid target path containing manifest.json and catalog.json.")
179
+ sys.exit(3)
180
+
181
+ manifest_path = os.path.join(target_path, "manifest.json")
182
+ catalog_path = os.path.join(target_path, "catalog.json")
183
+
184
+ # Display detected CI information
185
+ if ci_info:
186
+ console.rule("Detected CI Information", style="blue")
187
+ info_table = []
188
+ if ci_info.platform:
189
+ info_table.append(f"[cyan]Platform:[/cyan] {ci_info.platform}")
190
+
191
+ # Display CR number as PR or MR based on platform
192
+ if ci_info.cr_number is not None:
193
+ if ci_info.platform == "github-actions":
194
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.cr_number}")
195
+ elif ci_info.platform == "gitlab-ci":
196
+ info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.cr_number}")
197
+ else:
198
+ info_table.append(f"[cyan]CR Number:[/cyan] {ci_info.cr_number}")
199
+
200
+ # Display CR URL as PR URL or MR URL based on platform
201
+ if ci_info.cr_url:
202
+ if ci_info.platform == "github-actions":
203
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.cr_url}")
204
+ elif ci_info.platform == "gitlab-ci":
205
+ info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.cr_url}")
206
+ else:
207
+ info_table.append(f"[cyan]CR URL:[/cyan] {ci_info.cr_url}")
208
+
209
+ if ci_info.session_type:
210
+ info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
211
+ if ci_info.commit_sha:
212
+ info_table.append(f"[cyan]Commit SHA:[/cyan] {ci_info.commit_sha[:8]}...")
213
+ if ci_info.base_branch:
214
+ info_table.append(f"[cyan]Base Branch:[/cyan] {ci_info.base_branch}")
215
+ if ci_info.source_branch:
216
+ info_table.append(f"[cyan]Source Branch:[/cyan] {ci_info.source_branch}")
217
+ if ci_info.repository:
218
+ info_table.append(f"[cyan]Repository:[/cyan] {ci_info.repository}")
219
+
220
+ for line in info_table:
221
+ console.print(line)
222
+
223
+ # 3. Extract adapter type from manifest
224
+ try:
225
+ adapter_type = get_adapter_type(manifest_path)
226
+ except Exception as e:
227
+ console.print("[red]Error:[/red] Failed to parse adapter type from manifest.json")
228
+ console.print(f"Reason: {e}")
229
+ sys.exit(3)
230
+
231
+ # 4. Handle dry-run mode (before authentication or API calls)
232
+ if dry_run:
233
+ console.rule("Dry Run Summary", style="yellow")
234
+ console.print("[yellow]Dry run mode enabled - no actual upload will be performed[/yellow]")
235
+ console.print()
236
+
237
+ # Display platform information if detected
238
+ if ci_info and ci_info.platform:
239
+ console.print("[cyan]Platform Information:[/cyan]")
240
+ console.print(f" • Platform: {ci_info.platform}")
241
+ if ci_info.repository:
242
+ console.print(f" • Repository: {ci_info.repository}")
243
+ if ci_info.cr_number is not None:
244
+ console.print(f" • CR Number: {ci_info.cr_number}")
245
+ if ci_info.commit_sha:
246
+ console.print(f" • Commit SHA: {ci_info.commit_sha[:8]}")
247
+ if ci_info.source_branch:
248
+ console.print(f" • Source Branch: {ci_info.source_branch}")
249
+ if ci_info.base_branch:
250
+ console.print(f" • Base Branch: {ci_info.base_branch}")
251
+ if ci_info.session_type:
252
+ console.print(f" • Session Type: {ci_info.session_type}")
253
+ console.print()
254
+
255
+ # Display upload summary
256
+ console.print("[cyan]Upload Workflow:[/cyan]")
257
+ if session_id:
258
+ console.print(" • Upload to existing session")
259
+ console.print(f" • Session ID: {session_id}")
260
+ else:
261
+ console.print(" • Auto-create session and upload")
262
+ if ci_info and ci_info.platform in ["github-actions", "gitlab-ci"]:
263
+ console.print(" • Platform-specific APIs will be used")
264
+ else:
265
+ console.print(" • [yellow]Warning: Platform not supported for auto-session creation[/yellow]")
266
+
267
+ console.print()
268
+ console.print("[cyan]Files to upload:[/cyan]")
269
+ console.print(f" • manifest.json: {os.path.abspath(manifest_path)}")
270
+ console.print(f" • catalog.json: {os.path.abspath(catalog_path)}")
271
+ console.print(f" • Adapter type: {adapter_type}")
272
+
273
+ console.print()
274
+ console.print("[green]✓[/green] Dry run completed successfully")
275
+ sys.exit(0)
276
+
277
+ # 5. Get authentication token
278
+ token = os.getenv("RECCE_API_TOKEN")
279
+
280
+ # Fallback to CI-detected token if RECCE_API_TOKEN not set
281
+ if not token and ci_info and ci_info.access_token:
282
+ token = ci_info.access_token
283
+ if ci_info.platform == "github-actions":
284
+ console.print("[cyan]Info:[/cyan] Using GITHUB_TOKEN for authentication")
285
+ elif ci_info.platform == "gitlab-ci":
286
+ console.print("[cyan]Info:[/cyan] Using CI_JOB_TOKEN for authentication")
287
+
288
+ if not token:
289
+ console.print("[red]Error:[/red] No authentication token provided")
290
+ console.print("Set RECCE_API_TOKEN environment variable or ensure CI token is available")
291
+ sys.exit(2)
292
+
293
+ # 6. Choose upload workflow based on whether session_id is provided
294
+ if session_id:
295
+ # Generic workflow: Upload to existing session using session ID
296
+ upload_to_existing_session(console, token, session_id, manifest_path, catalog_path, adapter_type, target_path)
297
+ else:
298
+ # Platform-specific workflow: Use platform APIs to create session and upload
299
+ upload_with_platform_apis(console, token, ci_info, manifest_path, catalog_path, adapter_type, target_path)
300
+
301
+
302
+ if __name__ == "__main__":
303
+ cloud_cli()
recce_cloud/upload.py ADDED
@@ -0,0 +1,213 @@
1
+ """
2
+ Upload helper functions for recce-cloud CLI.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+
8
+ import requests
9
+
10
+ from recce_cloud.api.client import RecceCloudClient
11
+ from recce_cloud.api.exceptions import RecceCloudException
12
+ from recce_cloud.api.factory import create_platform_client
13
+
14
+
15
+ def upload_to_existing_session(
16
+ console, token: str, session_id: str, manifest_path: str, catalog_path: str, adapter_type: str, target_path: str
17
+ ):
18
+ """
19
+ Upload artifacts to an existing Recce Cloud session using session ID.
20
+
21
+ This is the generic workflow that requires a pre-existing session ID.
22
+ """
23
+ try:
24
+ client = RecceCloudClient(token)
25
+ except Exception as e:
26
+ console.print("[red]Error:[/red] Failed to initialize API client")
27
+ console.print(f"Reason: {e}")
28
+ sys.exit(2)
29
+
30
+ # Get session info (org_id, project_id)
31
+ console.print(f'Uploading artifacts for session ID "{session_id}"')
32
+ try:
33
+ session = client.get_session(session_id)
34
+ if session.get("status") == "error":
35
+ console.print(f"[red]Error:[/red] {session.get('message')}")
36
+ sys.exit(2)
37
+
38
+ org_id = session.get("org_id")
39
+ if org_id is None:
40
+ console.print(f"[red]Error:[/red] Session ID {session_id} does not belong to any organization.")
41
+ sys.exit(2)
42
+
43
+ project_id = session.get("project_id")
44
+ if project_id is None:
45
+ console.print(f"[red]Error:[/red] Session ID {session_id} does not belong to any project.")
46
+ sys.exit(2)
47
+
48
+ except RecceCloudException as e:
49
+ console.print("[red]Error:[/red] Failed to get session info")
50
+ console.print(f"Reason: {e.reason}")
51
+ sys.exit(2)
52
+ except Exception as e:
53
+ console.print("[red]Error:[/red] Failed to get session info")
54
+ console.print(f"Reason: {e}")
55
+ sys.exit(2)
56
+
57
+ # Get presigned URLs
58
+ try:
59
+ presigned_urls = client.get_upload_urls_by_session_id(org_id, project_id, session_id)
60
+ except RecceCloudException as e:
61
+ console.print("[red]Error:[/red] Failed to get upload URLs")
62
+ console.print(f"Reason: {e.reason}")
63
+ sys.exit(4)
64
+ except Exception as e:
65
+ console.print("[red]Error:[/red] Failed to get upload URLs")
66
+ console.print(f"Reason: {e}")
67
+ sys.exit(4)
68
+
69
+ # Upload manifest.json
70
+ console.print(f'Uploading manifest from path "{manifest_path}"')
71
+ try:
72
+ with open(manifest_path, "rb") as f:
73
+ response = requests.put(presigned_urls["manifest_url"], data=f.read())
74
+ if response.status_code not in [200, 204]:
75
+ raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
76
+ except Exception as e:
77
+ console.print("[red]Error:[/red] Failed to upload manifest.json")
78
+ console.print(f"Reason: {e}")
79
+ sys.exit(4)
80
+
81
+ # Upload catalog.json
82
+ console.print(f'Uploading catalog from path "{catalog_path}"')
83
+ try:
84
+ with open(catalog_path, "rb") as f:
85
+ response = requests.put(presigned_urls["catalog_url"], data=f.read())
86
+ if response.status_code not in [200, 204]:
87
+ raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
88
+ except Exception as e:
89
+ console.print("[red]Error:[/red] Failed to upload catalog.json")
90
+ console.print(f"Reason: {e}")
91
+ sys.exit(4)
92
+
93
+ # Update session metadata
94
+ try:
95
+ client.update_session(org_id, project_id, session_id, adapter_type)
96
+ except RecceCloudException as e:
97
+ console.print("[red]Error:[/red] Failed to update session metadata")
98
+ console.print(f"Reason: {e.reason}")
99
+ sys.exit(4)
100
+ except Exception as e:
101
+ console.print("[red]Error:[/red] Failed to update session metadata")
102
+ console.print(f"Reason: {e}")
103
+ sys.exit(4)
104
+
105
+ # Success!
106
+ console.rule("Uploaded Successfully", style="green")
107
+ console.print(
108
+ f'Uploaded dbt artifacts to Recce Cloud for session ID "{session_id}" from "{os.path.abspath(target_path)}"'
109
+ )
110
+ sys.exit(0)
111
+
112
+
113
+ def upload_with_platform_apis(
114
+ console, token: str, ci_info, manifest_path: str, catalog_path: str, adapter_type: str, target_path: str
115
+ ):
116
+ """
117
+ Upload artifacts using platform-specific APIs (GitHub Actions or GitLab CI).
118
+
119
+ This workflow uses touch-recce-session to create a session automatically.
120
+ """
121
+ # Validate platform support
122
+ if ci_info.platform not in ["github-actions", "gitlab-ci"]:
123
+ console.print("[red]Error:[/red] Platform-specific upload requires GitHub Actions or GitLab CI environment")
124
+ console.print(f"Detected platform: {ci_info.platform or 'unknown'}")
125
+ console.print(
126
+ "Either run this command in a supported CI environment or provide --session-id for generic upload"
127
+ )
128
+ sys.exit(1)
129
+
130
+ # Create platform-specific client
131
+ try:
132
+ client = create_platform_client(token, ci_info)
133
+ except ValueError as e:
134
+ console.print("[red]Error:[/red] Failed to create platform client")
135
+ console.print(f"Reason: {e}")
136
+ sys.exit(2)
137
+
138
+ # Touch session to create or get session ID
139
+ console.rule("Creating/touching session", style="blue")
140
+ try:
141
+ session_response = client.touch_recce_session(
142
+ branch=ci_info.source_branch or ci_info.base_branch or "main",
143
+ adapter_type=adapter_type,
144
+ cr_number=ci_info.cr_number,
145
+ commit_sha=ci_info.commit_sha,
146
+ )
147
+
148
+ session_id = session_response.get("session_id")
149
+ manifest_upload_url = session_response.get("manifest_upload_url")
150
+ catalog_upload_url = session_response.get("catalog_upload_url")
151
+
152
+ if not session_id or not manifest_upload_url or not catalog_upload_url:
153
+ console.print("[red]Error:[/red] Incomplete response from touch-recce-session API")
154
+ console.print(f"Response: {session_response}")
155
+ sys.exit(4)
156
+
157
+ console.print(f"[green]Session ID:[/green] {session_id}")
158
+
159
+ except RecceCloudException as e:
160
+ console.print("[red]Error:[/red] Failed to create/touch session")
161
+ console.print(f"Reason: {e.reason}")
162
+ sys.exit(4)
163
+ except Exception as e:
164
+ console.print("[red]Error:[/red] Failed to create/touch session")
165
+ console.print(f"Reason: {e}")
166
+ sys.exit(4)
167
+
168
+ # Upload manifest.json
169
+ console.print(f'Uploading manifest from path "{manifest_path}"')
170
+ try:
171
+ with open(manifest_path, "rb") as f:
172
+ response = requests.put(manifest_upload_url, data=f.read())
173
+ if response.status_code not in [200, 204]:
174
+ raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
175
+ except Exception as e:
176
+ console.print("[red]Error:[/red] Failed to upload manifest.json")
177
+ console.print(f"Reason: {e}")
178
+ sys.exit(4)
179
+
180
+ # Upload catalog.json
181
+ console.print(f'Uploading catalog from path "{catalog_path}"')
182
+ try:
183
+ with open(catalog_path, "rb") as f:
184
+ response = requests.put(catalog_upload_url, data=f.read())
185
+ if response.status_code not in [200, 204]:
186
+ raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
187
+ except Exception as e:
188
+ console.print("[red]Error:[/red] Failed to upload catalog.json")
189
+ console.print(f"Reason: {e}")
190
+ sys.exit(4)
191
+
192
+ # Notify upload completion
193
+ console.print("Notifying upload completion...")
194
+ try:
195
+ client.upload_completed(session_id=session_id, commit_sha=ci_info.commit_sha)
196
+ except RecceCloudException as e:
197
+ console.print("[yellow]Warning:[/yellow] Failed to notify upload completion")
198
+ console.print(f"Reason: {e.reason}")
199
+ # Non-fatal, continue
200
+ except Exception as e:
201
+ console.print("[yellow]Warning:[/yellow] Failed to notify upload completion")
202
+ console.print(f"Reason: {e}")
203
+ # Non-fatal, continue
204
+
205
+ # Success!
206
+ console.rule("Uploaded Successfully", style="green")
207
+ console.print(f'Uploaded dbt artifacts to Recce Cloud for session ID "{session_id}"')
208
+ console.print(f'Artifacts from: "{os.path.abspath(target_path)}"')
209
+
210
+ if ci_info.cr_url:
211
+ console.print(f"Change request: {ci_info.cr_url}")
212
+
213
+ sys.exit(0)