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/cli.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Recce Cloud CLI - Lightweight command for managing Recce Cloud operations.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import subprocess
|
|
@@ -26,6 +27,7 @@ from recce_cloud.download import (
|
|
|
26
27
|
download_with_platform_apis,
|
|
27
28
|
)
|
|
28
29
|
from recce_cloud.report import fetch_and_generate_report
|
|
30
|
+
from recce_cloud.review import run_review_command
|
|
29
31
|
from recce_cloud.upload import upload_to_existing_session, upload_with_platform_apis
|
|
30
32
|
|
|
31
33
|
# Configure logging
|
|
@@ -208,17 +210,19 @@ def init(org, project, status, clear):
|
|
|
208
210
|
console = Console()
|
|
209
211
|
|
|
210
212
|
# Check authentication first
|
|
211
|
-
token = get_api_token()
|
|
213
|
+
token = os.getenv("RECCE_API_TOKEN") or get_api_token()
|
|
212
214
|
if not token:
|
|
213
|
-
console.print("[red]Error:[/red]
|
|
214
|
-
console.print("
|
|
215
|
+
console.print("[red]Error:[/red] No RECCE_API_TOKEN provided and not logged in")
|
|
216
|
+
console.print("Either set RECCE_API_TOKEN environment variable or run 'recce-cloud login' first")
|
|
215
217
|
sys.exit(1)
|
|
216
218
|
|
|
217
219
|
# Status check mode
|
|
218
220
|
if status:
|
|
219
221
|
binding = get_project_binding()
|
|
220
222
|
if binding:
|
|
221
|
-
console.print(
|
|
223
|
+
console.print(
|
|
224
|
+
f"[green]✓[/green] Bound to org_id=[cyan]{binding['org_id']}[/cyan], project_id=[cyan]{binding['project_id']}[/cyan]"
|
|
225
|
+
)
|
|
222
226
|
if binding.get("bound_at"):
|
|
223
227
|
console.print(f" Bound at: {binding['bound_at']}")
|
|
224
228
|
if binding.get("bound_by"):
|
|
@@ -262,12 +266,14 @@ def init(org, project, status, clear):
|
|
|
262
266
|
console.print(f"[red]Error:[/red] Project '{project}' not found in organization '{org}'")
|
|
263
267
|
sys.exit(1)
|
|
264
268
|
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
# Store IDs (immutable) instead of slugs (can be renamed)
|
|
270
|
+
org_id = str(org_obj.get("id"))
|
|
271
|
+
project_id = str(project_obj.get("id"))
|
|
268
272
|
|
|
269
|
-
save_project_binding(
|
|
270
|
-
console.print(
|
|
273
|
+
save_project_binding(org_id, project_id, user_email)
|
|
274
|
+
console.print(
|
|
275
|
+
f"[green]✓[/green] Bound to org_id=[cyan]{org_id}[/cyan], project_id=[cyan]{project_id}[/cyan]"
|
|
276
|
+
)
|
|
271
277
|
console.print(f" Config saved to {get_config_path()}")
|
|
272
278
|
|
|
273
279
|
# Offer to add to .gitignore
|
|
@@ -323,16 +329,17 @@ def init(org, project, status, clear):
|
|
|
323
329
|
console.print("Please create a project at https://cloud.datarecce.io first")
|
|
324
330
|
sys.exit(1)
|
|
325
331
|
|
|
326
|
-
# Build project choices: (
|
|
332
|
+
# Build project choices: (project_id for config, display_name for UI)
|
|
327
333
|
# Filter out archived projects
|
|
328
334
|
project_choices = []
|
|
329
335
|
for p in projects:
|
|
330
336
|
# Skip archived projects (check status field and archived flags)
|
|
331
337
|
if p.get("status") == "archived" or p.get("archived") or p.get("is_archived"):
|
|
332
338
|
continue
|
|
333
|
-
|
|
339
|
+
project_id = str(p.get("id"))
|
|
340
|
+
project_name = p.get("name") or p.get("slug") or project_id
|
|
334
341
|
display_name = p.get("display_name") or project_name
|
|
335
|
-
project_choices.append((
|
|
342
|
+
project_choices.append((project_id, display_name))
|
|
336
343
|
|
|
337
344
|
if not project_choices:
|
|
338
345
|
console.print(f"[yellow]No active projects found in {selected_org_display}[/yellow]")
|
|
@@ -346,12 +353,14 @@ def init(org, project, status, clear):
|
|
|
346
353
|
console.print(f" {i}. {display_name}")
|
|
347
354
|
|
|
348
355
|
project_idx = click.prompt("Enter number", type=click.IntRange(1, len(project_choices)))
|
|
349
|
-
|
|
356
|
+
selected_project_id, selected_project_display = project_choices[project_idx - 1]
|
|
350
357
|
|
|
351
|
-
# Save binding (
|
|
352
|
-
save_project_binding(
|
|
358
|
+
# Save binding using IDs (immutable) instead of slugs (can be renamed)
|
|
359
|
+
save_project_binding(str(selected_org_id), selected_project_id, user_email)
|
|
353
360
|
console.print()
|
|
354
|
-
console.print(
|
|
361
|
+
console.print(
|
|
362
|
+
f"[green]✓[/green] Bound to org_id=[cyan]{selected_org_id}[/cyan], project_id=[cyan]{selected_project_id}[/cyan]"
|
|
363
|
+
)
|
|
355
364
|
console.print(f" Config saved to {get_config_path()}")
|
|
356
365
|
|
|
357
366
|
# Offer to add to .gitignore
|
|
@@ -381,44 +390,25 @@ def _get_production_session_id(console: Console, token: str) -> Optional[str]:
|
|
|
381
390
|
from recce_cloud.api.exceptions import RecceCloudException
|
|
382
391
|
from recce_cloud.config.project_config import get_project_binding
|
|
383
392
|
|
|
384
|
-
# Get project binding
|
|
393
|
+
# Get project binding (now stores IDs directly)
|
|
385
394
|
binding = get_project_binding()
|
|
386
395
|
if not binding:
|
|
387
|
-
# Check environment variables as fallback
|
|
396
|
+
# Check environment variables as fallback (accept both slugs and IDs)
|
|
388
397
|
env_org = os.environ.get("RECCE_ORG")
|
|
389
398
|
env_project = os.environ.get("RECCE_PROJECT")
|
|
390
399
|
if env_org and env_project:
|
|
391
|
-
binding = {"
|
|
400
|
+
binding = {"org_id": env_org, "project_id": env_project}
|
|
392
401
|
else:
|
|
393
402
|
console.print("[red]Error:[/red] No project binding found")
|
|
394
403
|
console.print("Run 'recce-cloud init' to bind this directory to a project")
|
|
395
404
|
return None
|
|
396
405
|
|
|
397
|
-
|
|
398
|
-
|
|
406
|
+
org_id = binding.get("org_id")
|
|
407
|
+
project_id = binding.get("project_id")
|
|
399
408
|
|
|
400
409
|
try:
|
|
401
410
|
client = RecceCloudClient(token)
|
|
402
411
|
|
|
403
|
-
# Get org and project IDs
|
|
404
|
-
org_info = client.get_organization(org_slug)
|
|
405
|
-
if not org_info:
|
|
406
|
-
console.print(f"[red]Error:[/red] Organization '{org_slug}' not found")
|
|
407
|
-
return None
|
|
408
|
-
org_id = org_info.get("id")
|
|
409
|
-
if not org_id:
|
|
410
|
-
console.print(f"[red]Error:[/red] Organization '{org_slug}' response missing ID")
|
|
411
|
-
return None
|
|
412
|
-
|
|
413
|
-
project_info = client.get_project(org_id, project_slug)
|
|
414
|
-
if not project_info:
|
|
415
|
-
console.print(f"[red]Error:[/red] Project '{project_slug}' not found")
|
|
416
|
-
return None
|
|
417
|
-
project_id = project_info.get("id")
|
|
418
|
-
if not project_id:
|
|
419
|
-
console.print(f"[red]Error:[/red] Project '{project_slug}' response missing ID")
|
|
420
|
-
return None
|
|
421
|
-
|
|
422
412
|
# List sessions and find production session
|
|
423
413
|
sessions = client.list_sessions(org_id, project_id)
|
|
424
414
|
for session in sessions:
|
|
@@ -474,14 +464,14 @@ def _get_production_session_id(console: Console, token: str) -> Optional[str]:
|
|
|
474
464
|
help="Skip confirmation prompts (auto-create session if not found).",
|
|
475
465
|
)
|
|
476
466
|
@click.option(
|
|
477
|
-
"--
|
|
467
|
+
"--pr",
|
|
478
468
|
type=int,
|
|
479
|
-
help="
|
|
469
|
+
help="Pull/Merge request number (PR/MR) (overrides auto-detection)",
|
|
480
470
|
)
|
|
481
471
|
@click.option(
|
|
482
472
|
"--type",
|
|
483
473
|
"session_type",
|
|
484
|
-
type=click.Choice(["
|
|
474
|
+
type=click.Choice(["pr", "prod", "dev"]),
|
|
485
475
|
help="Session type (overrides auto-detection)",
|
|
486
476
|
)
|
|
487
477
|
@click.option(
|
|
@@ -489,7 +479,7 @@ def _get_production_session_id(console: Console, token: str) -> Optional[str]:
|
|
|
489
479
|
is_flag=True,
|
|
490
480
|
help="Show what would be uploaded without actually uploading",
|
|
491
481
|
)
|
|
492
|
-
def upload(target_path, session_id, session_name, skip_confirmation,
|
|
482
|
+
def upload(target_path, session_id, session_name, skip_confirmation, pr, session_type, dry_run):
|
|
493
483
|
"""
|
|
494
484
|
Upload dbt artifacts (manifest.json, catalog.json) to Recce Cloud.
|
|
495
485
|
|
|
@@ -525,7 +515,7 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
|
|
|
525
515
|
console.rule("CI Environment Detection", style="blue")
|
|
526
516
|
try:
|
|
527
517
|
ci_info = CIDetector.detect()
|
|
528
|
-
ci_info = CIDetector.apply_overrides(ci_info,
|
|
518
|
+
ci_info = CIDetector.apply_overrides(ci_info, pr=pr, session_type=session_type)
|
|
529
519
|
|
|
530
520
|
# Display detected CI information immediately
|
|
531
521
|
if ci_info:
|
|
@@ -534,22 +524,22 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
|
|
|
534
524
|
info_table.append(f"[cyan]Platform:[/cyan] {ci_info.platform}")
|
|
535
525
|
|
|
536
526
|
# Display CR number as PR or MR based on platform
|
|
537
|
-
if ci_info.
|
|
527
|
+
if ci_info.pr_number is not None:
|
|
538
528
|
if ci_info.platform == "github-actions":
|
|
539
|
-
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.
|
|
529
|
+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
|
|
540
530
|
elif ci_info.platform == "gitlab-ci":
|
|
541
|
-
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.
|
|
531
|
+
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.pr_number}")
|
|
542
532
|
else:
|
|
543
|
-
info_table.append(f"[cyan]
|
|
533
|
+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
|
|
544
534
|
|
|
545
|
-
# Display
|
|
546
|
-
if ci_info.
|
|
535
|
+
# Display PR URL as PR URL or MR URL based on platform
|
|
536
|
+
if ci_info.pr_url:
|
|
547
537
|
if ci_info.platform == "github-actions":
|
|
548
|
-
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.
|
|
538
|
+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
|
|
549
539
|
elif ci_info.platform == "gitlab-ci":
|
|
550
|
-
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.
|
|
540
|
+
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.pr_url}")
|
|
551
541
|
else:
|
|
552
|
-
info_table.append(f"[cyan]
|
|
542
|
+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
|
|
553
543
|
|
|
554
544
|
if ci_info.session_type:
|
|
555
545
|
info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
|
|
@@ -600,8 +590,8 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
|
|
|
600
590
|
console.print(f" • Platform: {ci_info.platform}")
|
|
601
591
|
if ci_info.repository:
|
|
602
592
|
console.print(f" • Repository: {ci_info.repository}")
|
|
603
|
-
if ci_info.
|
|
604
|
-
console.print(f" •
|
|
593
|
+
if ci_info.pr_number is not None:
|
|
594
|
+
console.print(f" • PR Number: {ci_info.pr_number}")
|
|
605
595
|
if ci_info.commit_sha:
|
|
606
596
|
console.print(f" • Commit SHA: {ci_info.commit_sha[:8]}")
|
|
607
597
|
if ci_info.source_branch:
|
|
@@ -719,8 +709,8 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
|
|
|
719
709
|
@click.option(
|
|
720
710
|
"--type",
|
|
721
711
|
"session_type",
|
|
722
|
-
type=click.Choice(["
|
|
723
|
-
help="Filter by session type (prod=base,
|
|
712
|
+
type=click.Choice(["pr", "prod", "dev"]),
|
|
713
|
+
help="Filter by session type (prod=base, pr=has PR link, dev=other)",
|
|
724
714
|
)
|
|
725
715
|
@click.option(
|
|
726
716
|
"--json",
|
|
@@ -748,8 +738,6 @@ def list_sessions_cmd(session_type, output_json):
|
|
|
748
738
|
# Output as JSON
|
|
749
739
|
recce-cloud list --json
|
|
750
740
|
"""
|
|
751
|
-
import json
|
|
752
|
-
|
|
753
741
|
from rich.table import Table
|
|
754
742
|
|
|
755
743
|
from recce_cloud.api.client import RecceCloudClient
|
|
@@ -768,8 +756,8 @@ def list_sessions_cmd(session_type, output_json):
|
|
|
768
756
|
# 2. Resolve org/project configuration
|
|
769
757
|
try:
|
|
770
758
|
config = resolve_config()
|
|
771
|
-
org = config.
|
|
772
|
-
project = config.
|
|
759
|
+
org = config.org_id
|
|
760
|
+
project = config.project_id
|
|
773
761
|
except ConfigurationError as e:
|
|
774
762
|
console.print("[red]Error:[/red] Could not resolve org/project configuration")
|
|
775
763
|
console.print(f"Reason: {e}")
|
|
@@ -808,13 +796,13 @@ def list_sessions_cmd(session_type, output_json):
|
|
|
808
796
|
|
|
809
797
|
# Helper to derive session type from fields:
|
|
810
798
|
# - prod: is_base = True
|
|
811
|
-
# -
|
|
799
|
+
# - pr: pr_link is not null
|
|
812
800
|
# - dev: everything else
|
|
813
801
|
def get_session_type(s):
|
|
814
802
|
if s.get("is_base"):
|
|
815
803
|
return "prod"
|
|
816
804
|
elif s.get("pr_link"):
|
|
817
|
-
return "
|
|
805
|
+
return "pr"
|
|
818
806
|
else:
|
|
819
807
|
return "dev"
|
|
820
808
|
|
|
@@ -957,22 +945,22 @@ def download(target_path, session_id, prod, dry_run, force):
|
|
|
957
945
|
info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
|
|
958
946
|
|
|
959
947
|
# Only show CR number and URL for CR sessions (not for prod)
|
|
960
|
-
if ci_info.session_type == "
|
|
948
|
+
if ci_info.session_type == "pr" and ci_info.pr_number is not None:
|
|
961
949
|
if ci_info.platform == "github-actions":
|
|
962
|
-
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.
|
|
950
|
+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
|
|
963
951
|
elif ci_info.platform == "gitlab-ci":
|
|
964
|
-
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.
|
|
952
|
+
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.pr_number}")
|
|
965
953
|
else:
|
|
966
|
-
info_table.append(f"[cyan]
|
|
954
|
+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
|
|
967
955
|
|
|
968
|
-
# Only show
|
|
969
|
-
if ci_info.session_type == "
|
|
956
|
+
# Only show PR URL for CR sessions
|
|
957
|
+
if ci_info.session_type == "pr" and ci_info.pr_url:
|
|
970
958
|
if ci_info.platform == "github-actions":
|
|
971
|
-
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.
|
|
959
|
+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
|
|
972
960
|
elif ci_info.platform == "gitlab-ci":
|
|
973
|
-
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.
|
|
961
|
+
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.pr_url}")
|
|
974
962
|
else:
|
|
975
|
-
info_table.append(f"[cyan]
|
|
963
|
+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
|
|
976
964
|
|
|
977
965
|
for line in info_table:
|
|
978
966
|
console.print(line)
|
|
@@ -997,8 +985,8 @@ def download(target_path, session_id, prod, dry_run, force):
|
|
|
997
985
|
console.print(f" • Repository: {ci_info.repository}")
|
|
998
986
|
if ci_info.session_type:
|
|
999
987
|
console.print(f" • Session Type: {ci_info.session_type}")
|
|
1000
|
-
if ci_info.session_type == "
|
|
1001
|
-
console.print(f" •
|
|
988
|
+
if ci_info.session_type == "pr" and ci_info.pr_number is not None:
|
|
989
|
+
console.print(f" • PR Number: {ci_info.pr_number}")
|
|
1002
990
|
console.print()
|
|
1003
991
|
|
|
1004
992
|
# Display download summary
|
|
@@ -1124,22 +1112,22 @@ def delete(session_id, dry_run, force):
|
|
|
1124
1112
|
info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
|
|
1125
1113
|
|
|
1126
1114
|
# Only show CR number and URL for CR sessions (not for prod)
|
|
1127
|
-
if ci_info.session_type == "
|
|
1115
|
+
if ci_info.session_type == "pr" and ci_info.pr_number is not None:
|
|
1128
1116
|
if ci_info.platform == "github-actions":
|
|
1129
|
-
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.
|
|
1117
|
+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
|
|
1130
1118
|
elif ci_info.platform == "gitlab-ci":
|
|
1131
|
-
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.
|
|
1119
|
+
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.pr_number}")
|
|
1132
1120
|
else:
|
|
1133
|
-
info_table.append(f"[cyan]
|
|
1121
|
+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
|
|
1134
1122
|
|
|
1135
|
-
# Only show
|
|
1136
|
-
if ci_info.session_type == "
|
|
1123
|
+
# Only show PR URL for CR sessions
|
|
1124
|
+
if ci_info.session_type == "pr" and ci_info.pr_url:
|
|
1137
1125
|
if ci_info.platform == "github-actions":
|
|
1138
|
-
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.
|
|
1126
|
+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
|
|
1139
1127
|
elif ci_info.platform == "gitlab-ci":
|
|
1140
|
-
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.
|
|
1128
|
+
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.pr_url}")
|
|
1141
1129
|
else:
|
|
1142
|
-
info_table.append(f"[cyan]
|
|
1130
|
+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
|
|
1143
1131
|
|
|
1144
1132
|
for line in info_table:
|
|
1145
1133
|
console.print(line)
|
|
@@ -1164,8 +1152,8 @@ def delete(session_id, dry_run, force):
|
|
|
1164
1152
|
console.print(f" • Repository: {ci_info.repository}")
|
|
1165
1153
|
if ci_info.session_type:
|
|
1166
1154
|
console.print(f" • Session Type: {ci_info.session_type}")
|
|
1167
|
-
if ci_info.session_type == "
|
|
1168
|
-
console.print(f" •
|
|
1155
|
+
if ci_info.session_type == "pr" and ci_info.pr_number is not None:
|
|
1156
|
+
console.print(f" • PR Number: {ci_info.pr_number}")
|
|
1169
1157
|
console.print()
|
|
1170
1158
|
|
|
1171
1159
|
# Display delete summary
|
|
@@ -1355,5 +1343,175 @@ def report(repo, since, until, base_branch, merged_only, output):
|
|
|
1355
1343
|
sys.exit(exit_code)
|
|
1356
1344
|
|
|
1357
1345
|
|
|
1346
|
+
@cloud_cli.command()
|
|
1347
|
+
@click.option(
|
|
1348
|
+
"--session-id",
|
|
1349
|
+
envvar="RECCE_SESSION_ID",
|
|
1350
|
+
help="Session ID to generate data review for (or use RECCE_SESSION_ID env var). "
|
|
1351
|
+
"Mutually exclusive with --session-name.",
|
|
1352
|
+
)
|
|
1353
|
+
@click.option(
|
|
1354
|
+
"--session-name",
|
|
1355
|
+
help="Name of the session to generate data review for. " "Mutually exclusive with --session-id.",
|
|
1356
|
+
)
|
|
1357
|
+
@click.option(
|
|
1358
|
+
"--org",
|
|
1359
|
+
default=None,
|
|
1360
|
+
help="Organization name or slug (auto-detected from project binding if not provided)",
|
|
1361
|
+
)
|
|
1362
|
+
@click.option(
|
|
1363
|
+
"--project",
|
|
1364
|
+
default=None,
|
|
1365
|
+
help="Project name or slug (auto-detected from project binding if not provided)",
|
|
1366
|
+
)
|
|
1367
|
+
@click.option(
|
|
1368
|
+
"--regenerate",
|
|
1369
|
+
is_flag=True,
|
|
1370
|
+
help="Force regeneration even if a data review already exists",
|
|
1371
|
+
)
|
|
1372
|
+
@click.option(
|
|
1373
|
+
"--timeout",
|
|
1374
|
+
type=int,
|
|
1375
|
+
default=300,
|
|
1376
|
+
help="Maximum seconds to wait for review generation (default: 300)",
|
|
1377
|
+
)
|
|
1378
|
+
@click.option(
|
|
1379
|
+
"--json",
|
|
1380
|
+
"json_output",
|
|
1381
|
+
is_flag=True,
|
|
1382
|
+
help="Output result as JSON (for scripting)",
|
|
1383
|
+
)
|
|
1384
|
+
def review(session_id, session_name, org, project, regenerate, timeout, json_output):
|
|
1385
|
+
"""
|
|
1386
|
+
Generate a data review for a session.
|
|
1387
|
+
|
|
1388
|
+
Data reviews provide AI-generated insights comparing your session's data
|
|
1389
|
+
with the production baseline. This command triggers review generation and
|
|
1390
|
+
waits for completion.
|
|
1391
|
+
|
|
1392
|
+
\b
|
|
1393
|
+
Prerequisites:
|
|
1394
|
+
- The session must exist and have artifacts uploaded
|
|
1395
|
+
- A base (production) session must exist with artifacts uploaded
|
|
1396
|
+
- You must be logged in or have RECCE_API_TOKEN set
|
|
1397
|
+
|
|
1398
|
+
\b
|
|
1399
|
+
Authentication:
|
|
1400
|
+
- RECCE_API_TOKEN env var or 'recce-cloud login' profile
|
|
1401
|
+
|
|
1402
|
+
\b
|
|
1403
|
+
Examples:
|
|
1404
|
+
# Generate review for a session by name (uses project binding)
|
|
1405
|
+
recce-cloud review --session-name my-pr-session
|
|
1406
|
+
|
|
1407
|
+
# Generate review for a session by ID
|
|
1408
|
+
recce-cloud review --session-id abc123def456
|
|
1409
|
+
|
|
1410
|
+
# Explicit org/project specification
|
|
1411
|
+
recce-cloud review --session-name my-session --org myorg --project myproject
|
|
1412
|
+
|
|
1413
|
+
# Force regeneration of existing review
|
|
1414
|
+
recce-cloud review --session-name my-session --regenerate
|
|
1415
|
+
|
|
1416
|
+
# JSON output for CI/CD scripting
|
|
1417
|
+
recce-cloud review --session-name my-session --json
|
|
1418
|
+
|
|
1419
|
+
# Custom timeout (10 minutes)
|
|
1420
|
+
recce-cloud review --session-name my-session --timeout 600
|
|
1421
|
+
"""
|
|
1422
|
+
console = Console()
|
|
1423
|
+
|
|
1424
|
+
# Validate that at least one of session_id or session_name is provided
|
|
1425
|
+
if not session_id and not session_name:
|
|
1426
|
+
if json_output:
|
|
1427
|
+
print(
|
|
1428
|
+
json.dumps(
|
|
1429
|
+
{
|
|
1430
|
+
"success": False,
|
|
1431
|
+
"error": "Either --session-id or --session-name must be provided",
|
|
1432
|
+
}
|
|
1433
|
+
)
|
|
1434
|
+
)
|
|
1435
|
+
else:
|
|
1436
|
+
console.print("[red]Error:[/red] Either --session-id or --session-name must be provided")
|
|
1437
|
+
sys.exit(1)
|
|
1438
|
+
|
|
1439
|
+
# Warn if both are provided (session-id takes precedence)
|
|
1440
|
+
if session_id and session_name:
|
|
1441
|
+
if not json_output:
|
|
1442
|
+
console.print(
|
|
1443
|
+
"[yellow]Warning:[/yellow] Both --session-id and --session-name provided. " "Using --session-id."
|
|
1444
|
+
)
|
|
1445
|
+
session_name = None # Clear session_name to use session_id
|
|
1446
|
+
|
|
1447
|
+
# 1. Get API token
|
|
1448
|
+
from recce_cloud.auth.profile import get_api_token
|
|
1449
|
+
|
|
1450
|
+
token = os.getenv("RECCE_API_TOKEN") or get_api_token()
|
|
1451
|
+
if not token:
|
|
1452
|
+
if json_output:
|
|
1453
|
+
print(
|
|
1454
|
+
json.dumps(
|
|
1455
|
+
{
|
|
1456
|
+
"success": False,
|
|
1457
|
+
"error": "No RECCE_API_TOKEN provided and not logged in",
|
|
1458
|
+
}
|
|
1459
|
+
)
|
|
1460
|
+
)
|
|
1461
|
+
else:
|
|
1462
|
+
console.print("[red]Error:[/red] No RECCE_API_TOKEN provided and not logged in")
|
|
1463
|
+
console.print("Either set RECCE_API_TOKEN environment variable or run 'recce-cloud login' first")
|
|
1464
|
+
sys.exit(2)
|
|
1465
|
+
|
|
1466
|
+
# 2. Resolve org/project configuration
|
|
1467
|
+
from recce_cloud.config.resolver import ConfigurationError, resolve_config
|
|
1468
|
+
|
|
1469
|
+
try:
|
|
1470
|
+
config = resolve_config(cli_org=org, cli_project=project)
|
|
1471
|
+
org_id = config.org_id
|
|
1472
|
+
project_id = config.project_id
|
|
1473
|
+
except ConfigurationError as e:
|
|
1474
|
+
if json_output:
|
|
1475
|
+
print(
|
|
1476
|
+
json.dumps(
|
|
1477
|
+
{
|
|
1478
|
+
"success": False,
|
|
1479
|
+
"error": str(e),
|
|
1480
|
+
}
|
|
1481
|
+
)
|
|
1482
|
+
)
|
|
1483
|
+
else:
|
|
1484
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1485
|
+
console.print()
|
|
1486
|
+
console.print("Provide --org and --project options, or run 'recce-cloud init' to bind to a project")
|
|
1487
|
+
sys.exit(1)
|
|
1488
|
+
|
|
1489
|
+
if not json_output:
|
|
1490
|
+
console.rule("Data Review", style="blue")
|
|
1491
|
+
console.print(f"[cyan]Organization:[/cyan] {org_id}")
|
|
1492
|
+
console.print(f"[cyan]Project:[/cyan] {project_id}")
|
|
1493
|
+
if session_id:
|
|
1494
|
+
session_id_display = session_id[:8] + "..." if len(session_id) > 8 else session_id
|
|
1495
|
+
console.print(f"[cyan]Session ID:[/cyan] {session_id_display}")
|
|
1496
|
+
else:
|
|
1497
|
+
console.print(f"[cyan]Session:[/cyan] {session_name}")
|
|
1498
|
+
if regenerate:
|
|
1499
|
+
console.print("[yellow]Regenerate mode enabled[/yellow]")
|
|
1500
|
+
console.print()
|
|
1501
|
+
|
|
1502
|
+
# 3. Run the review command
|
|
1503
|
+
run_review_command(
|
|
1504
|
+
console=console,
|
|
1505
|
+
token=token,
|
|
1506
|
+
org_id=org_id,
|
|
1507
|
+
project_id=project_id,
|
|
1508
|
+
session_name=session_name,
|
|
1509
|
+
session_id=session_id,
|
|
1510
|
+
regenerate=regenerate,
|
|
1511
|
+
timeout=timeout,
|
|
1512
|
+
json_output=json_output,
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
|
|
1358
1516
|
if __name__ == "__main__":
|
|
1359
1517
|
cloud_cli()
|
|
@@ -57,11 +57,11 @@ class DiagnosticRenderer:
|
|
|
57
57
|
check = results.project_binding
|
|
58
58
|
|
|
59
59
|
if check.passed:
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
org_id = check.details.get("org_id")
|
|
61
|
+
project_id = check.details.get("project_id")
|
|
62
62
|
source = check.details.get("source")
|
|
63
63
|
source_label = " (via env vars)" if source == "env_vars" else ""
|
|
64
|
-
self.console.print(f"[green]✓[/green] Bound to [cyan]{
|
|
64
|
+
self.console.print(f"[green]✓[/green] Bound to [cyan]{org_id}/{project_id}[/cyan]{source_label}")
|
|
65
65
|
else:
|
|
66
66
|
self._render_failure(check)
|
|
67
67
|
|
|
@@ -87,26 +87,26 @@ def get_project_binding(project_dir: Optional[str] = None) -> Optional[Dict[str,
|
|
|
87
87
|
project_dir: Project directory path. Defaults to current directory.
|
|
88
88
|
|
|
89
89
|
Returns:
|
|
90
|
-
Dictionary with
|
|
90
|
+
Dictionary with org_id, project_id, bound_at, bound_by fields,
|
|
91
91
|
or None if not bound.
|
|
92
92
|
"""
|
|
93
93
|
config = load_config(project_dir)
|
|
94
94
|
cloud_config = config.get("cloud", {})
|
|
95
95
|
|
|
96
|
-
if not cloud_config.get("
|
|
96
|
+
if not cloud_config.get("org_id") or not cloud_config.get("project_id"):
|
|
97
97
|
return None
|
|
98
98
|
|
|
99
99
|
return {
|
|
100
|
-
"
|
|
101
|
-
"
|
|
100
|
+
"org_id": cloud_config.get("org_id"),
|
|
101
|
+
"project_id": cloud_config.get("project_id"),
|
|
102
102
|
"bound_at": cloud_config.get("bound_at"),
|
|
103
103
|
"bound_by": cloud_config.get("bound_by"),
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
def save_project_binding(
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
org_id: str,
|
|
109
|
+
project_id: str,
|
|
110
110
|
bound_by: Optional[str] = None,
|
|
111
111
|
project_dir: Optional[str] = None,
|
|
112
112
|
) -> None:
|
|
@@ -114,8 +114,8 @@ def save_project_binding(
|
|
|
114
114
|
Save project binding to .recce/config.
|
|
115
115
|
|
|
116
116
|
Args:
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
org_id: Organization ID.
|
|
118
|
+
project_id: Project ID.
|
|
119
119
|
bound_by: Email of user who created the binding.
|
|
120
120
|
project_dir: Project directory path. Defaults to current directory.
|
|
121
121
|
"""
|
|
@@ -123,8 +123,8 @@ def save_project_binding(
|
|
|
123
123
|
|
|
124
124
|
config["version"] = 1
|
|
125
125
|
config["cloud"] = {
|
|
126
|
-
"
|
|
127
|
-
"
|
|
126
|
+
"org_id": org_id,
|
|
127
|
+
"project_id": project_id,
|
|
128
128
|
"bound_at": datetime.now(timezone.utc).isoformat(),
|
|
129
129
|
}
|
|
130
130
|
|