recce-cloud 1.33.1__py3-none-any.whl → 1.34.0__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_cloud/VERSION +1 -1
- recce_cloud/api/base.py +9 -9
- recce_cloud/api/client.py +257 -5
- recce_cloud/api/github.py +19 -19
- recce_cloud/api/gitlab.py +19 -19
- recce_cloud/ci_providers/base.py +8 -8
- recce_cloud/ci_providers/detector.py +17 -17
- recce_cloud/ci_providers/github_actions.py +8 -8
- recce_cloud/ci_providers/gitlab_ci.py +8 -8
- recce_cloud/cli.py +244 -86
- recce_cloud/commands/diagnostics.py +3 -3
- recce_cloud/config/project_config.py +10 -10
- recce_cloud/config/resolver.py +47 -19
- recce_cloud/delete.py +6 -6
- recce_cloud/download.py +5 -5
- recce_cloud/review.py +541 -0
- recce_cloud/services/diagnostic_service.py +7 -7
- recce_cloud/upload.py +5 -5
- {recce_cloud-1.33.1.dist-info → recce_cloud-1.34.0.dist-info}/METADATA +6 -6
- recce_cloud-1.34.0.dist-info/RECORD +38 -0
- recce_cloud-1.33.1.dist-info/RECORD +0 -37
- {recce_cloud-1.33.1.dist-info → recce_cloud-1.34.0.dist-info}/WHEEL +0 -0
- {recce_cloud-1.33.1.dist-info → recce_cloud-1.34.0.dist-info}/entry_points.txt +0 -0
recce_cloud/review.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data review helper functions for recce-cloud CLI.
|
|
3
|
+
|
|
4
|
+
Handles the business logic for generating and retrieving data reviews.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from recce_cloud.api.client import RecceCloudClient
|
|
16
|
+
from recce_cloud.api.exceptions import RecceCloudException
|
|
17
|
+
|
|
18
|
+
# Cloud UI configuration - use same defaults as auth/login.py
|
|
19
|
+
RECCE_CLOUD_API_HOST = os.environ.get("RECCE_CLOUD_API_HOST", "https://cloud.datarecce.io")
|
|
20
|
+
RECCE_CLOUD_BASE_URL = os.environ.get("RECCE_CLOUD_BASE_URL", RECCE_CLOUD_API_HOST)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReviewStatus(Enum):
|
|
24
|
+
"""Status of the data review generation.
|
|
25
|
+
|
|
26
|
+
These align with Recce Cloud UI/backend task statuses where applicable.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Terminal states (align with backend RecceTaskStatus)
|
|
30
|
+
SUCCEEDED = "SUCCEEDED"
|
|
31
|
+
FAILED = "FAILED"
|
|
32
|
+
|
|
33
|
+
# CLI-specific states
|
|
34
|
+
ALREADY_EXISTS = "ALREADY_EXISTS"
|
|
35
|
+
TIMEOUT = "TIMEOUT"
|
|
36
|
+
|
|
37
|
+
# In-progress states (align with backend RecceTaskStatus)
|
|
38
|
+
QUEUED = "QUEUED"
|
|
39
|
+
SCHEDULED = "SCHEDULED"
|
|
40
|
+
RUNNING = "RUNNING"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ReviewResult:
|
|
45
|
+
"""Result of a data review operation."""
|
|
46
|
+
|
|
47
|
+
status: ReviewStatus
|
|
48
|
+
session_id: Optional[str] = None
|
|
49
|
+
session_name: Optional[str] = None
|
|
50
|
+
review_url: Optional[str] = None
|
|
51
|
+
task_id: Optional[str] = None
|
|
52
|
+
error_message: Optional[str] = None
|
|
53
|
+
summary: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Default polling configuration
|
|
57
|
+
DEFAULT_POLL_INTERVAL_SECONDS = 5
|
|
58
|
+
DEFAULT_TIMEOUT_SECONDS = 300 # 5 minutes
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_review_url(
|
|
62
|
+
org_id: str,
|
|
63
|
+
project_id: str,
|
|
64
|
+
session_id: str,
|
|
65
|
+
client: Optional[RecceCloudClient] = None,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Generate the URL to view the data review.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
org_id: Organization ID.
|
|
72
|
+
project_id: Project ID.
|
|
73
|
+
session_id: Session ID.
|
|
74
|
+
client: Optional RecceCloudClient to fetch current slugs from API.
|
|
75
|
+
If provided, the URL will use current slugs (handles renames).
|
|
76
|
+
If not provided, falls back to using IDs directly.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
URL string to view the data review.
|
|
80
|
+
"""
|
|
81
|
+
# Default to using IDs
|
|
82
|
+
org_slug = org_id
|
|
83
|
+
project_slug = project_id
|
|
84
|
+
|
|
85
|
+
# If client provided, fetch current slugs from API
|
|
86
|
+
if client:
|
|
87
|
+
try:
|
|
88
|
+
org = client.get_organization(org_id)
|
|
89
|
+
if org:
|
|
90
|
+
org_slug = org.get("name") or org.get("slug") or org_id
|
|
91
|
+
|
|
92
|
+
project = client.get_project(org_id, project_id)
|
|
93
|
+
if project:
|
|
94
|
+
project_slug = project.get("name") or project.get("slug") or project_id
|
|
95
|
+
except Exception:
|
|
96
|
+
# If API call fails, fall back to using IDs
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Use the Recce Cloud web UI URL (respects RECCE_CLOUD_BASE_URL env var)
|
|
100
|
+
return f"{RECCE_CLOUD_BASE_URL}/{org_slug}/{project_slug}/{session_id}/review"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def check_prerequisites(
|
|
104
|
+
client: RecceCloudClient,
|
|
105
|
+
org_id: str,
|
|
106
|
+
project_id: str,
|
|
107
|
+
session_name: Optional[str] = None,
|
|
108
|
+
session_id: Optional[str] = None,
|
|
109
|
+
) -> Dict[str, Any]:
|
|
110
|
+
"""
|
|
111
|
+
Check prerequisites for data review generation.
|
|
112
|
+
|
|
113
|
+
This function calls the backend API which verifies:
|
|
114
|
+
1. Session must exist
|
|
115
|
+
2. Session must have artifacts uploaded
|
|
116
|
+
3. Base session must exist
|
|
117
|
+
4. Base session must have artifacts uploaded
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
client: RecceCloudClient instance.
|
|
121
|
+
org_id: Organization ID or slug.
|
|
122
|
+
project_id: Project ID or slug.
|
|
123
|
+
session_name: Session name to check (used if session_id not provided).
|
|
124
|
+
session_id: Session ID to check directly (takes precedence over session_name).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
dict with:
|
|
128
|
+
- success: bool indicating if all prerequisites are met
|
|
129
|
+
- session: session dict if found (with id, name, adapter_type)
|
|
130
|
+
- error: error message if prerequisites not met
|
|
131
|
+
"""
|
|
132
|
+
result = {
|
|
133
|
+
"success": False,
|
|
134
|
+
"session": None,
|
|
135
|
+
"error": None,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# If session_id is not provided, look up by session_name
|
|
139
|
+
if not session_id:
|
|
140
|
+
if not session_name:
|
|
141
|
+
result["error"] = "Either session_id or session_name must be provided"
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
session = client.get_session_by_name(org_id, project_id, session_name)
|
|
146
|
+
if not session:
|
|
147
|
+
result["error"] = f"Session '{session_name}' not found in project"
|
|
148
|
+
return result
|
|
149
|
+
session_id = session.get("id")
|
|
150
|
+
except RecceCloudException as e:
|
|
151
|
+
result["error"] = f"Failed to find session: {e.reason}"
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
# Call the backend API to check all prerequisites
|
|
155
|
+
try:
|
|
156
|
+
prereq_response = client.check_prerequisites(org_id, project_id, session_id)
|
|
157
|
+
|
|
158
|
+
# Build session dict from the response
|
|
159
|
+
result["session"] = {
|
|
160
|
+
"id": prereq_response.get("session_id"),
|
|
161
|
+
"name": prereq_response.get("session_name"),
|
|
162
|
+
"adapter_type": prereq_response.get("adapter_type"),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if prereq_response.get("is_ready"):
|
|
166
|
+
result["success"] = True
|
|
167
|
+
else:
|
|
168
|
+
result["error"] = prereq_response.get("reason", "Prerequisites not met")
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
except RecceCloudException as e:
|
|
173
|
+
result["error"] = f"Failed to check prerequisites: {e.reason}"
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def poll_task_status(
|
|
178
|
+
client: RecceCloudClient,
|
|
179
|
+
org_id: str,
|
|
180
|
+
task_id: str,
|
|
181
|
+
poll_interval: int = DEFAULT_POLL_INTERVAL_SECONDS,
|
|
182
|
+
timeout: int = DEFAULT_TIMEOUT_SECONDS,
|
|
183
|
+
progress_callback=None,
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
Poll task status until completion or timeout.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
client: RecceCloudClient instance.
|
|
190
|
+
org_id: Organization ID or slug.
|
|
191
|
+
task_id: Task ID to poll.
|
|
192
|
+
poll_interval: Seconds between polls.
|
|
193
|
+
timeout: Maximum seconds to wait.
|
|
194
|
+
progress_callback: Optional callback(status_dict) called on each poll.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
dict with:
|
|
198
|
+
- success: bool indicating if task completed successfully
|
|
199
|
+
- status: final task status string
|
|
200
|
+
- error: error message if failed
|
|
201
|
+
- task: full task status dict
|
|
202
|
+
"""
|
|
203
|
+
start_time = time.time()
|
|
204
|
+
last_status = None
|
|
205
|
+
|
|
206
|
+
while True:
|
|
207
|
+
elapsed = time.time() - start_time
|
|
208
|
+
if elapsed > timeout:
|
|
209
|
+
return {
|
|
210
|
+
"success": False,
|
|
211
|
+
"status": "timeout",
|
|
212
|
+
"error": f"Task timed out after {timeout} seconds",
|
|
213
|
+
"task": None,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
task_status = client.get_task_status(org_id, task_id)
|
|
218
|
+
except RecceCloudException as e:
|
|
219
|
+
return {
|
|
220
|
+
"success": False,
|
|
221
|
+
"status": "error",
|
|
222
|
+
"error": f"Failed to get task status: {e.reason}",
|
|
223
|
+
"task": None,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
status = task_status.get("status", "unknown")
|
|
227
|
+
|
|
228
|
+
# Call progress callback if status changed
|
|
229
|
+
if progress_callback and status != last_status:
|
|
230
|
+
progress_callback(task_status)
|
|
231
|
+
last_status = status
|
|
232
|
+
|
|
233
|
+
# Check terminal states (handle both lowercase and uppercase from backend)
|
|
234
|
+
if status.lower() in ["completed", "succeeded"]:
|
|
235
|
+
return {
|
|
236
|
+
"success": True,
|
|
237
|
+
"status": status,
|
|
238
|
+
"error": None,
|
|
239
|
+
"task": task_status,
|
|
240
|
+
}
|
|
241
|
+
elif status.lower() in ["failed", "error", "cancelled"]:
|
|
242
|
+
error_msg = task_status.get("metadata", {}).get("error", f"Task {status}")
|
|
243
|
+
return {
|
|
244
|
+
"success": False,
|
|
245
|
+
"status": status,
|
|
246
|
+
"error": error_msg,
|
|
247
|
+
"task": task_status,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Still running, wait and poll again
|
|
251
|
+
time.sleep(poll_interval)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def generate_data_review(
|
|
255
|
+
console,
|
|
256
|
+
client: RecceCloudClient,
|
|
257
|
+
org_id: str,
|
|
258
|
+
project_id: str,
|
|
259
|
+
session: Dict[str, Any],
|
|
260
|
+
regenerate: bool = False,
|
|
261
|
+
timeout: int = DEFAULT_TIMEOUT_SECONDS,
|
|
262
|
+
json_output: bool = False,
|
|
263
|
+
) -> ReviewResult:
|
|
264
|
+
"""
|
|
265
|
+
Generate data review for a session.
|
|
266
|
+
|
|
267
|
+
This function handles:
|
|
268
|
+
1. Checking if a review already exists
|
|
269
|
+
2. Checking if a task is already running
|
|
270
|
+
3. Triggering new review generation
|
|
271
|
+
4. Polling for completion
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
console: Rich console for output.
|
|
275
|
+
client: RecceCloudClient instance.
|
|
276
|
+
org_id: Organization ID or slug.
|
|
277
|
+
project_id: Project ID or slug.
|
|
278
|
+
session: Session dict from API.
|
|
279
|
+
regenerate: If True, regenerate even if review exists.
|
|
280
|
+
timeout: Maximum seconds to wait for generation.
|
|
281
|
+
json_output: If True, suppress progress output.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
ReviewResult with status and details.
|
|
285
|
+
"""
|
|
286
|
+
session_id = session["id"]
|
|
287
|
+
session_name = session.get("name", session_id)
|
|
288
|
+
|
|
289
|
+
# 1. Check if review already exists (skip if regenerating)
|
|
290
|
+
# Note: get_data_review() returns None for 404 (no review), so we don't need try-except for that case.
|
|
291
|
+
# Let other errors (403, 500) propagate - they indicate real problems that would also fail generation.
|
|
292
|
+
if not regenerate:
|
|
293
|
+
existing_review = client.get_data_review(org_id, project_id, session_id)
|
|
294
|
+
if existing_review and existing_review.get("summary"):
|
|
295
|
+
return ReviewResult(
|
|
296
|
+
status=ReviewStatus.ALREADY_EXISTS,
|
|
297
|
+
session_id=session_id,
|
|
298
|
+
session_name=session_name,
|
|
299
|
+
review_url=generate_review_url(org_id, project_id, session_id, client),
|
|
300
|
+
summary=existing_review.get("summary"),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# 2. Check if a task is already running
|
|
304
|
+
# Note: get_running_task() returns None for 404 (no running task), so we don't need try-except.
|
|
305
|
+
# Let other errors (403, 500) propagate - they indicate real problems.
|
|
306
|
+
running_task = client.get_running_task(org_id, project_id, session_id)
|
|
307
|
+
if running_task:
|
|
308
|
+
task_id = running_task["task_id"]
|
|
309
|
+
if not json_output:
|
|
310
|
+
console.print(f"[yellow]Task already running:[/yellow] {task_id}")
|
|
311
|
+
console.print("Waiting for completion...")
|
|
312
|
+
else:
|
|
313
|
+
task_id = None
|
|
314
|
+
|
|
315
|
+
# 3. Trigger new generation if no task running
|
|
316
|
+
if not task_id:
|
|
317
|
+
if not json_output:
|
|
318
|
+
action = "Regenerating" if regenerate else "Generating"
|
|
319
|
+
console.print(f"[cyan]{action} data review...[/cyan]")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
response = client.generate_data_review(org_id, project_id, session_id, regenerate=regenerate)
|
|
323
|
+
task_id = response.get("task_id")
|
|
324
|
+
|
|
325
|
+
if not task_id:
|
|
326
|
+
# Review was already generated and no new task needed
|
|
327
|
+
existing_review = client.get_data_review(org_id, project_id, session_id)
|
|
328
|
+
if existing_review and existing_review.get("summary"):
|
|
329
|
+
return ReviewResult(
|
|
330
|
+
status=ReviewStatus.ALREADY_EXISTS,
|
|
331
|
+
session_id=session_id,
|
|
332
|
+
session_name=session_name,
|
|
333
|
+
review_url=generate_review_url(org_id, project_id, session_id, client),
|
|
334
|
+
summary=existing_review.get("summary"),
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
return ReviewResult(
|
|
338
|
+
status=ReviewStatus.FAILED,
|
|
339
|
+
session_id=session_id,
|
|
340
|
+
session_name=session_name,
|
|
341
|
+
error_message="No task created and no existing review found",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
except RecceCloudException as e:
|
|
345
|
+
return ReviewResult(
|
|
346
|
+
status=ReviewStatus.FAILED,
|
|
347
|
+
session_id=session_id,
|
|
348
|
+
session_name=session_name,
|
|
349
|
+
error_message=f"Failed to trigger review generation: {e.reason}",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# 4. Poll for completion
|
|
353
|
+
def progress_callback(task_status):
|
|
354
|
+
if not json_output:
|
|
355
|
+
# Display status as-is from backend (aligned with Recce Cloud UI)
|
|
356
|
+
status = task_status.get("status", "unknown")
|
|
357
|
+
console.print(f" Status: [cyan]{status}[/cyan]")
|
|
358
|
+
|
|
359
|
+
if not json_output:
|
|
360
|
+
console.print(f"[cyan]Task ID:[/cyan] {task_id}")
|
|
361
|
+
console.print("Waiting for review generation to complete...")
|
|
362
|
+
|
|
363
|
+
poll_result = poll_task_status(
|
|
364
|
+
client,
|
|
365
|
+
org_id,
|
|
366
|
+
task_id,
|
|
367
|
+
timeout=timeout,
|
|
368
|
+
progress_callback=progress_callback if not json_output else None,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if poll_result["success"]:
|
|
372
|
+
# Fetch the generated review
|
|
373
|
+
try:
|
|
374
|
+
review = client.get_data_review(org_id, project_id, session_id)
|
|
375
|
+
return ReviewResult(
|
|
376
|
+
status=ReviewStatus.SUCCEEDED,
|
|
377
|
+
session_id=session_id,
|
|
378
|
+
session_name=session_name,
|
|
379
|
+
review_url=generate_review_url(org_id, project_id, session_id, client),
|
|
380
|
+
task_id=task_id,
|
|
381
|
+
summary=review.get("summary") if review else None,
|
|
382
|
+
)
|
|
383
|
+
except RecceCloudException as e:
|
|
384
|
+
return ReviewResult(
|
|
385
|
+
status=ReviewStatus.FAILED,
|
|
386
|
+
session_id=session_id,
|
|
387
|
+
session_name=session_name,
|
|
388
|
+
task_id=task_id,
|
|
389
|
+
error_message=f"Review generated but failed to fetch: {e.reason}",
|
|
390
|
+
)
|
|
391
|
+
elif poll_result["status"] == "timeout":
|
|
392
|
+
return ReviewResult(
|
|
393
|
+
status=ReviewStatus.TIMEOUT,
|
|
394
|
+
session_id=session_id,
|
|
395
|
+
session_name=session_name,
|
|
396
|
+
task_id=task_id,
|
|
397
|
+
error_message=poll_result["error"],
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
return ReviewResult(
|
|
401
|
+
status=ReviewStatus.FAILED,
|
|
402
|
+
session_id=session_id,
|
|
403
|
+
session_name=session_name,
|
|
404
|
+
task_id=task_id,
|
|
405
|
+
error_message=poll_result["error"],
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def run_review_command(
|
|
410
|
+
console,
|
|
411
|
+
token: str,
|
|
412
|
+
org_id: str,
|
|
413
|
+
project_id: str,
|
|
414
|
+
session_name: Optional[str] = None,
|
|
415
|
+
session_id: Optional[str] = None,
|
|
416
|
+
regenerate: bool = False,
|
|
417
|
+
timeout: int = DEFAULT_TIMEOUT_SECONDS,
|
|
418
|
+
json_output: bool = False,
|
|
419
|
+
):
|
|
420
|
+
"""
|
|
421
|
+
Main entry point for the review command.
|
|
422
|
+
|
|
423
|
+
This orchestrates the full review workflow:
|
|
424
|
+
1. Initialize client
|
|
425
|
+
2. Check prerequisites
|
|
426
|
+
3. Generate review
|
|
427
|
+
4. Display results
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
console: Rich console for output.
|
|
431
|
+
token: API token.
|
|
432
|
+
org_id: Organization ID or slug.
|
|
433
|
+
project_id: Project ID or slug.
|
|
434
|
+
session_name: Session name to generate review for (used if session_id not provided).
|
|
435
|
+
session_id: Session ID to generate review for (takes precedence over session_name).
|
|
436
|
+
regenerate: If True, regenerate even if review exists.
|
|
437
|
+
timeout: Maximum seconds to wait for generation.
|
|
438
|
+
json_output: If True, output JSON instead of human-readable text.
|
|
439
|
+
"""
|
|
440
|
+
# 1. Initialize client
|
|
441
|
+
try:
|
|
442
|
+
client = RecceCloudClient(token)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
if json_output:
|
|
445
|
+
print(json.dumps({"success": False, "error": f"Failed to initialize client: {e}"}))
|
|
446
|
+
else:
|
|
447
|
+
console.print("[red]Error:[/red] Failed to initialize API client")
|
|
448
|
+
console.print(f"Reason: {e}")
|
|
449
|
+
sys.exit(2)
|
|
450
|
+
|
|
451
|
+
# 2. Check prerequisites
|
|
452
|
+
if not json_output:
|
|
453
|
+
console.rule("Checking Prerequisites", style="blue")
|
|
454
|
+
|
|
455
|
+
prereq = check_prerequisites(client, org_id, project_id, session_name=session_name, session_id=session_id)
|
|
456
|
+
|
|
457
|
+
if not prereq["success"]:
|
|
458
|
+
if json_output:
|
|
459
|
+
output = {
|
|
460
|
+
"success": False,
|
|
461
|
+
"error": prereq["error"],
|
|
462
|
+
}
|
|
463
|
+
if session_name:
|
|
464
|
+
output["session_name"] = session_name
|
|
465
|
+
if session_id:
|
|
466
|
+
output["session_id"] = session_id
|
|
467
|
+
print(json.dumps(output))
|
|
468
|
+
else:
|
|
469
|
+
console.print(f"[red]Error:[/red] {prereq['error']}")
|
|
470
|
+
sys.exit(3)
|
|
471
|
+
|
|
472
|
+
session = prereq["session"]
|
|
473
|
+
resolved_session_id = session["id"]
|
|
474
|
+
resolved_session_name = session.get("name") or session_id or "(unnamed)"
|
|
475
|
+
|
|
476
|
+
if not json_output:
|
|
477
|
+
console.print(f"[green]✓[/green] Session found: {resolved_session_name}")
|
|
478
|
+
console.print(f" Session ID: {resolved_session_id}")
|
|
479
|
+
console.print(f" Adapter: {session.get('adapter_type', 'unknown')}")
|
|
480
|
+
console.print("[green]✓[/green] Base session available")
|
|
481
|
+
|
|
482
|
+
# 3. Generate review
|
|
483
|
+
if not json_output:
|
|
484
|
+
console.rule("Generating Data Review", style="blue")
|
|
485
|
+
|
|
486
|
+
result = generate_data_review(
|
|
487
|
+
console=console,
|
|
488
|
+
client=client,
|
|
489
|
+
org_id=org_id,
|
|
490
|
+
project_id=project_id,
|
|
491
|
+
session=session,
|
|
492
|
+
regenerate=regenerate,
|
|
493
|
+
timeout=timeout,
|
|
494
|
+
json_output=json_output,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# 4. Display results
|
|
498
|
+
if json_output:
|
|
499
|
+
output = {
|
|
500
|
+
"success": result.status in [ReviewStatus.SUCCEEDED, ReviewStatus.ALREADY_EXISTS],
|
|
501
|
+
"status": result.status.value,
|
|
502
|
+
"session_id": result.session_id,
|
|
503
|
+
"session_name": result.session_name,
|
|
504
|
+
"review_url": result.review_url,
|
|
505
|
+
}
|
|
506
|
+
if result.task_id:
|
|
507
|
+
output["task_id"] = result.task_id
|
|
508
|
+
if result.error_message:
|
|
509
|
+
output["error"] = result.error_message
|
|
510
|
+
print(json.dumps(output))
|
|
511
|
+
else:
|
|
512
|
+
if result.status == ReviewStatus.SUCCEEDED:
|
|
513
|
+
console.rule("Review Generated Successfully", style="green")
|
|
514
|
+
console.print(f"[green]✓[/green] Data review generated for session '{result.session_name}'")
|
|
515
|
+
console.print()
|
|
516
|
+
console.print(f"[cyan]View review at:[/cyan] {result.review_url}")
|
|
517
|
+
elif result.status == ReviewStatus.ALREADY_EXISTS:
|
|
518
|
+
console.rule("Review Already Exists", style="green")
|
|
519
|
+
console.print(f"[green]✓[/green] Data review already exists for session '{result.session_name}'")
|
|
520
|
+
console.print()
|
|
521
|
+
console.print(f"[cyan]View review at:[/cyan] {result.review_url}")
|
|
522
|
+
console.print()
|
|
523
|
+
console.print("[dim]Tip: Use --regenerate to create a new review[/dim]")
|
|
524
|
+
elif result.status == ReviewStatus.TIMEOUT:
|
|
525
|
+
console.rule("Review Generation Timeout", style="yellow")
|
|
526
|
+
console.print("[yellow]Warning:[/yellow] Review generation is still in progress")
|
|
527
|
+
console.print(f"Task ID: {result.task_id}")
|
|
528
|
+
console.print("The review will be available shortly. Check the web UI for status.")
|
|
529
|
+
console.print()
|
|
530
|
+
console.print(f"[cyan]Check status at:[/cyan] {result.review_url}")
|
|
531
|
+
else:
|
|
532
|
+
console.rule("Review Generation Failed", style="red")
|
|
533
|
+
console.print(f"[red]Error:[/red] {result.error_message}")
|
|
534
|
+
|
|
535
|
+
# Exit with appropriate code
|
|
536
|
+
if result.status in [ReviewStatus.SUCCEEDED, ReviewStatus.ALREADY_EXISTS]:
|
|
537
|
+
sys.exit(0)
|
|
538
|
+
elif result.status == ReviewStatus.TIMEOUT:
|
|
539
|
+
sys.exit(0) # Timeout is not a hard failure - task is still running
|
|
540
|
+
else:
|
|
541
|
+
sys.exit(4)
|
|
@@ -172,18 +172,18 @@ class DiagnosticService:
|
|
|
172
172
|
binding = get_project_binding()
|
|
173
173
|
|
|
174
174
|
if binding:
|
|
175
|
-
self._org = binding.get("
|
|
176
|
-
self._project = binding.get("
|
|
175
|
+
self._org = binding.get("org_id")
|
|
176
|
+
self._project = binding.get("project_id")
|
|
177
177
|
return CheckResult(
|
|
178
178
|
status=CheckStatus.PASS,
|
|
179
179
|
details={
|
|
180
|
-
"
|
|
181
|
-
"
|
|
180
|
+
"org_id": self._org,
|
|
181
|
+
"project_id": self._project,
|
|
182
182
|
"source": "config_file",
|
|
183
183
|
},
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
# Check environment variables as fallback
|
|
186
|
+
# Check environment variables as fallback (accept both slugs and IDs)
|
|
187
187
|
env_org = os.environ.get("RECCE_ORG")
|
|
188
188
|
env_project = os.environ.get("RECCE_PROJECT")
|
|
189
189
|
|
|
@@ -193,8 +193,8 @@ class DiagnosticService:
|
|
|
193
193
|
return CheckResult(
|
|
194
194
|
status=CheckStatus.PASS,
|
|
195
195
|
details={
|
|
196
|
-
"
|
|
197
|
-
"
|
|
196
|
+
"org_id": self._org,
|
|
197
|
+
"project_id": self._project,
|
|
198
198
|
"source": "env_vars",
|
|
199
199
|
},
|
|
200
200
|
)
|
recce_cloud/upload.py
CHANGED
|
@@ -159,7 +159,7 @@ def upload_with_platform_apis(
|
|
|
159
159
|
session_response = client.touch_recce_session(
|
|
160
160
|
branch=ci_info.source_branch or ci_info.base_branch or "main",
|
|
161
161
|
adapter_type=adapter_type,
|
|
162
|
-
|
|
162
|
+
pr_number=ci_info.pr_number,
|
|
163
163
|
commit_sha=ci_info.commit_sha,
|
|
164
164
|
session_type=ci_info.session_type,
|
|
165
165
|
)
|
|
@@ -226,8 +226,8 @@ def upload_with_platform_apis(
|
|
|
226
226
|
console.print(f'Uploaded dbt artifacts to Recce Cloud for session ID "{session_id}"')
|
|
227
227
|
console.print(f'Artifacts from: "{os.path.abspath(target_path)}"')
|
|
228
228
|
|
|
229
|
-
if ci_info.
|
|
230
|
-
console.print(f"
|
|
229
|
+
if ci_info.pr_url:
|
|
230
|
+
console.print(f"Pull request: {ci_info.pr_url}")
|
|
231
231
|
|
|
232
232
|
sys.exit(0)
|
|
233
233
|
|
|
@@ -256,8 +256,8 @@ def upload_with_session_name(
|
|
|
256
256
|
console.rule("Session Name Resolution", style="blue")
|
|
257
257
|
try:
|
|
258
258
|
config = resolve_config()
|
|
259
|
-
org = config.
|
|
260
|
-
project = config.
|
|
259
|
+
org = config.org_id
|
|
260
|
+
project = config.project_id
|
|
261
261
|
console.print(f"[cyan]Organization:[/cyan] {org}")
|
|
262
262
|
console.print(f"[cyan]Project:[/cyan] {project}")
|
|
263
263
|
console.print(f"[cyan]Config Source:[/cyan] {config.source}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: recce-cloud
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.34.0
|
|
4
4
|
Summary: Lightweight CLI for Recce Cloud operations
|
|
5
5
|
Project-URL: Bug Tracker, https://github.com/InfuseAI/recce/issues
|
|
6
6
|
Author-email: InfuseAI Dev Team <dev@infuseai.io>
|
|
@@ -159,7 +159,7 @@ recce-cloud upload
|
|
|
159
159
|
recce-cloud upload --target-path custom-target
|
|
160
160
|
|
|
161
161
|
# With manual overrides
|
|
162
|
-
recce-cloud upload --
|
|
162
|
+
recce-cloud upload --pr 123 --type pr
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
**Requirements:**
|
|
@@ -355,8 +355,8 @@ Upload dbt artifacts to Recce Cloud session.
|
|
|
355
355
|
| --------------- | ------- | -------- | --------------------------------------------- |
|
|
356
356
|
| `--target-path` | path | `target` | Path to dbt target directory |
|
|
357
357
|
| `--session-id` | string | - | Session ID for generic workflow (optional) |
|
|
358
|
-
| `--
|
|
359
|
-
| `--type` | choice | - | Override session type: `
|
|
358
|
+
| `--pr` | integer | - | Override PR/MR number |
|
|
359
|
+
| `--type` | choice | - | Override session type: `pr`, `prod`, `dev` |
|
|
360
360
|
| `--dry-run` | flag | false | Show what would be uploaded without uploading |
|
|
361
361
|
|
|
362
362
|
**Environment Variables:**
|
|
@@ -492,13 +492,13 @@ You can override auto-detected values:
|
|
|
492
492
|
|
|
493
493
|
```bash
|
|
494
494
|
# Override PR/MR number
|
|
495
|
-
recce-cloud upload --
|
|
495
|
+
recce-cloud upload --pr 456
|
|
496
496
|
|
|
497
497
|
# Override session type
|
|
498
498
|
recce-cloud upload --type prod
|
|
499
499
|
|
|
500
500
|
# Multiple overrides
|
|
501
|
-
recce-cloud upload --
|
|
501
|
+
recce-cloud upload --pr 789 --type pr
|
|
502
502
|
|
|
503
503
|
# Dry run - preview what would be uploaded
|
|
504
504
|
recce-cloud upload --dry-run
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
recce_cloud/VERSION,sha256=xBVSUVA8aWplLFRKB0ZS35rrhaZQwe68qZ7iCifdheg,7
|
|
2
|
+
recce_cloud/__init__.py,sha256=2Zlm9V1IBJ9DxovzHIUJB0QDl6ergO5ktY09baQ-Kxc,907
|
|
3
|
+
recce_cloud/artifact.py,sha256=C6q1i8d0JdTIVLcJY1ZEmJJ0uBz-0g3zVZD8ss0K1iI,1458
|
|
4
|
+
recce_cloud/cli.py,sha256=eFS4WQkL-1K7gdSe4Cc9g6y-8QbcTc-5CgaKJMFGfe4,56652
|
|
5
|
+
recce_cloud/delete.py,sha256=sZSJkCLcTkiI2qVRUZwV5vqqV3BRzzuRUKdTLlVZfr0,3751
|
|
6
|
+
recce_cloud/download.py,sha256=bNBkh2tKLdFoQIuZNXG_wcP46VPCO_WlVszSmHAseGE,8482
|
|
7
|
+
recce_cloud/report.py,sha256=MRlT-CGV-SBzzYepp1zzoFuHqomC2vhpn7s5ACoNyyQ,10214
|
|
8
|
+
recce_cloud/review.py,sha256=0r4AT0Y7y-VjSs2Xxm1A79WChbreY9FYQKu-8oBcCXs,19059
|
|
9
|
+
recce_cloud/upload.py,sha256=SYYb4bJ4tfgXYUGSqvMMwLCgnmE2to02L2c1-z_5btQ,17543
|
|
10
|
+
recce_cloud/api/__init__.py,sha256=HrGOh7wp_U3G3gOXyGjJo1WoDXrMvhwjw8VGO2aKNTw,562
|
|
11
|
+
recce_cloud/api/base.py,sha256=aSjxqKuyrT5YHxP_NzwGqoBwwK0pXDEPu9DdHUdNm2I,4857
|
|
12
|
+
recce_cloud/api/client.py,sha256=qd3NqcTrm7WvTtmAqhE1pigGkNsMeBD7kX_CF6s1bXs,33096
|
|
13
|
+
recce_cloud/api/exceptions.py,sha256=BSfuh7dGZsx1qDkr2ZhRIAoPo_eXkAIhyho5PrUX2yo,634
|
|
14
|
+
recce_cloud/api/factory.py,sha256=0hdo2jTdTlvd8cU7YTZFJJ2q3ybSssA2JKLr0bO1QmQ,2081
|
|
15
|
+
recce_cloud/api/github.py,sha256=tMvUSq47aCdS8xgbgDmfUUD_JcGkdTslnWk5GemyMxE,4362
|
|
16
|
+
recce_cloud/api/gitlab.py,sha256=z9CobQUYwXkE8ke0yPRLsfRSfFPrd2y6UWJh9UiA5d4,4593
|
|
17
|
+
recce_cloud/auth/__init__.py,sha256=UP4yCMSmLNm9UhJBt80ybduZdPrAE3S2FYaZUocdh7E,453
|
|
18
|
+
recce_cloud/auth/callback_server.py,sha256=F2ph89mfdUMJYeTd9Nt5Jd-j7FECCRhn9cQcotIFlN0,3861
|
|
19
|
+
recce_cloud/auth/login.py,sha256=9t5AU1e8F7zNxfpjAQampxUkGhAV_9GwjKOG25QEo_U,7704
|
|
20
|
+
recce_cloud/auth/profile.py,sha256=hLQzPlIDb5MDUkxWZyheXVjaC4vCCL4PyqGsEm5FVvQ,3145
|
|
21
|
+
recce_cloud/auth/templates/error.html,sha256=MlirKaUGIc89QPPjqI3uQ-4sMbEy6k4JwS3YsDhzFFg,95074
|
|
22
|
+
recce_cloud/auth/templates/success.html,sha256=7tZttX6oFJWNW9R3y5xlOzNcECINj-lJvN_NcgW8puI,95092
|
|
23
|
+
recce_cloud/ci_providers/__init__.py,sha256=cB2bydn4QOMurJVfWhIN9uTD9gIyiBcYOloN-0TvMW4,328
|
|
24
|
+
recce_cloud/ci_providers/base.py,sha256=0R5M6GNV-9u2gPkeB9fmV0WgTC0U8-WUkB9Io33eGQs,2589
|
|
25
|
+
recce_cloud/ci_providers/detector.py,sha256=kyJx1Tkt6HXz8ZGwm4lcIeLMo0uXo8R1T2s0Fu4JNsY,5060
|
|
26
|
+
recce_cloud/ci_providers/github_actions.py,sha256=8Be3FcnHBUHwXpfdtuKFJNVlZYEe5oxiF-nUBGBhN34,4302
|
|
27
|
+
recce_cloud/ci_providers/gitlab_ci.py,sha256=vMFdqbdk2yxGnDEtsjeEbjIdqFZeai47W_NLLZA0NSQ,4175
|
|
28
|
+
recce_cloud/commands/__init__.py,sha256=H5LHyMEOc2mrFxu56lQlvrG_AJ_rxj0gnHjT2efGb2w,36
|
|
29
|
+
recce_cloud/commands/diagnostics.py,sha256=NqH8l0Xx90dwNVMvGKtFvszHvtQOcyFrWTDDf-Q3oXI,6073
|
|
30
|
+
recce_cloud/config/__init__.py,sha256=-26T9sXcsfHi1mAFCz6t19hy2WaK7ZYQlcGSPKhpcHU,423
|
|
31
|
+
recce_cloud/config/project_config.py,sha256=sfToZjP1TZNnSr31x0pwpLc_2DBZywrcqNpYqG1Wj3s,5269
|
|
32
|
+
recce_cloud/config/resolver.py,sha256=EYsWX0dwKHVKYf6kftekrbWd17vrALMUMl-T26up4VM,4712
|
|
33
|
+
recce_cloud/services/__init__.py,sha256=GMxN0Wtf166qvSSMGmU5jwqfly5b3K0vsIe8PkX7AKo,36
|
|
34
|
+
recce_cloud/services/diagnostic_service.py,sha256=X3YBHFTkB31WRIH9MCKpZxbl4drw9evD6bBiD8cDIkw,13049
|
|
35
|
+
recce_cloud-1.34.0.dist-info/METADATA,sha256=4TjurNAzv-MoZrNEVnzMS1JPm4juK9EqzaL_ZavJdjo,22589
|
|
36
|
+
recce_cloud-1.34.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
recce_cloud-1.34.0.dist-info/entry_points.txt,sha256=MVzgsEDWaQ4fWf33zlyPF83wg5Rgg5Axq2hlLEf0Yxg,58
|
|
38
|
+
recce_cloud-1.34.0.dist-info/RECORD,,
|