qbraid-cli 0.12.0a2__tar.gz → 0.12.0a3__tar.gz

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 (52) hide show
  1. {qbraid_cli-0.12.0a2/qbraid_cli.egg-info → qbraid_cli-0.12.0a3}/PKG-INFO +1 -1
  2. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/pyproject.toml +1 -1
  3. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/envs/app.py +739 -93
  4. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3/qbraid_cli.egg-info}/PKG-INFO +1 -1
  5. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/LICENSE +0 -0
  6. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/README.md +0 -0
  7. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/__init__.py +0 -0
  8. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/_version.py +0 -0
  9. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/account/__init__.py +0 -0
  10. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/account/app.py +0 -0
  11. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/admin/__init__.py +0 -0
  12. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/admin/app.py +0 -0
  13. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/admin/headers.py +0 -0
  14. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/admin/validation.py +0 -0
  15. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/chat/__init__.py +0 -0
  16. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/chat/app.py +0 -0
  17. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/configure/__init__.py +0 -0
  18. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/configure/actions.py +0 -0
  19. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/configure/app.py +0 -0
  20. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/configure/claude_config.py +0 -0
  21. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/devices/__init__.py +0 -0
  22. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/devices/app.py +0 -0
  23. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/devices/validation.py +0 -0
  24. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/envs/__init__.py +0 -0
  25. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/envs/activate.py +0 -0
  26. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/envs/create.py +0 -0
  27. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/envs/data_handling.py +0 -0
  28. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/exceptions.py +0 -0
  29. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/files/__init__.py +0 -0
  30. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/files/app.py +0 -0
  31. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/handlers.py +0 -0
  32. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/jobs/__init__.py +0 -0
  33. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/jobs/app.py +0 -0
  34. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/jobs/toggle_braket.py +0 -0
  35. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/jobs/validation.py +0 -0
  36. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/kernels/__init__.py +0 -0
  37. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/kernels/app.py +0 -0
  38. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/main.py +0 -0
  39. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/mcp/__init__.py +0 -0
  40. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/mcp/app.py +0 -0
  41. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/mcp/serve.py +0 -0
  42. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/pip/__init__.py +0 -0
  43. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/pip/app.py +0 -0
  44. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/pip/hooks.py +0 -0
  45. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli/py.typed +0 -0
  46. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli.egg-info/SOURCES.txt +0 -0
  47. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  48. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli.egg-info/entry_points.txt +0 -0
  49. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli.egg-info/requires.txt +0 -0
  50. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/qbraid_cli.egg-info/top_level.txt +0 -0
  51. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/setup.cfg +0 -0
  52. {qbraid_cli-0.12.0a2 → qbraid_cli-0.12.0a3}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.12.0a2
3
+ Version: 0.12.0a3
4
4
  Summary: Command Line Interface for interacting with all parts of the qBraid platform.
5
5
  Author-email: qBraid Development Team <contact@qbraid.com>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qbraid-cli"
7
- version = "0.12.0a2"
7
+ version = "0.12.0a3"
8
8
  description = "Command Line Interface for interacting with all parts of the qBraid platform."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }]
@@ -440,6 +440,179 @@ def envs_available():
440
440
  console.print("[dim]Create custom environments with: qbraid envs create[/dim]")
441
441
 
442
442
 
443
+ @envs_app.command(name="info")
444
+ def envs_info(
445
+ identifier: str = typer.Argument(..., help="Environment identifier (env_id or slug)"),
446
+ ):
447
+ """
448
+ Get detailed information about an environment.
449
+
450
+ Accepts either a local env_id or a cloud slug. Shows information from
451
+ both local registry and cloud catalog when available.
452
+
453
+ Examples:
454
+ $ qbraid envs info qiskit_1.0 # By slug (cloud + local if installed)
455
+ $ qbraid envs info a1b2 # By env_id (local only)
456
+ $ qbraid envs info my_custom_abc123 # Custom environment
457
+ """
458
+ from qbraid_core.services.environments import EnvironmentRegistryManager
459
+
460
+ console = Console()
461
+
462
+ # Try to find in local registry first
463
+ local_entry = None
464
+ local_entries = []
465
+ slug_to_query = None
466
+
467
+ try:
468
+ registry = EnvironmentRegistryManager()
469
+
470
+ # First try by env_id (exact match)
471
+ local_entry = registry.find_by_env_id(identifier)
472
+ if local_entry:
473
+ local_entries = [local_entry]
474
+ slug_to_query = local_entry.slug # May be None for local-only envs
475
+ else:
476
+ # Try by slug (may return multiple installations)
477
+ local_entries = registry.find_all_by_slug(identifier)
478
+ if local_entries:
479
+ slug_to_query = identifier
480
+ else:
481
+ # Not found locally, try cloud with this identifier as slug
482
+ slug_to_query = identifier
483
+ except Exception:
484
+ # Registry not available - try cloud only
485
+ slug_to_query = identifier
486
+
487
+ # Try to fetch from cloud API
488
+ cloud_env = None
489
+ if slug_to_query:
490
+ try:
491
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
492
+
493
+ def get_cloud_info():
494
+ client = EnvironmentManagerClient()
495
+ return client.get_environment_by_slug(slug_to_query)
496
+
497
+ cloud_env = run_progress_task(
498
+ get_cloud_info,
499
+ description="Fetching environment details...",
500
+ error_message="Failed to fetch from cloud",
501
+ )
502
+ except Exception:
503
+ # Cloud lookup failed - that's OK if we have local info
504
+ pass
505
+
506
+ # If nothing found anywhere, exit with error
507
+ if not local_entries and not cloud_env:
508
+ console.print(f"[red]❌ Environment not found:[/red] {identifier}")
509
+ console.print("[dim]Not found in local registry or cloud catalog.[/dim]")
510
+ raise typer.Exit(1)
511
+
512
+ # --- Display Cloud Info (if available) ---
513
+ if cloud_env:
514
+ name = cloud_env.get("displayName") or cloud_env.get("name") or "N/A"
515
+ description = cloud_env.get("description") or "No description"
516
+ owner = cloud_env.get("owner") or cloud_env.get("userId") or "Unknown"
517
+ python_version = cloud_env.get("pythonVersion") or "Unknown"
518
+ visibility = cloud_env.get("visibility") or "private"
519
+ tags = cloud_env.get("tags") or []
520
+ image_tag = cloud_env.get("imageTag") or cloud_env.get("image_tag") or "N/A"
521
+ available = cloud_env.get("available_for_installation", False)
522
+
523
+ visibility_colors = {"public": "green", "private": "yellow", "shared": "cyan"}
524
+ vis_color = visibility_colors.get(visibility, "white")
525
+
526
+ console.print(f"\n📦 [bold]{name}[/bold]")
527
+ console.print(f" [dim]{description}[/dim]")
528
+
529
+ console.print(f"\n[bold]☁️ Cloud Info:[/bold]")
530
+ console.print(f" Slug: {slug_to_query}")
531
+ console.print(f" Owner: {owner}")
532
+ console.print(f" Python: {python_version}")
533
+ console.print(f" Visibility: [{vis_color}]{visibility}[/{vis_color}]")
534
+ console.print(f" Image Tag: {image_tag}")
535
+
536
+ if tags:
537
+ console.print(f" Tags: {', '.join(tags)}")
538
+
539
+ # Publish status
540
+ review_status = cloud_env.get("reviewStatus")
541
+ if review_status:
542
+ status_colors = {
543
+ "none": "dim",
544
+ "requested": "yellow",
545
+ "pending": "cyan",
546
+ "approved": "green",
547
+ "denied": "red",
548
+ }
549
+ color = status_colors.get(review_status, "white")
550
+ console.print(f" Publish Status: [{color}]{review_status}[/{color}]")
551
+
552
+ # --- Display Local Info (if available) ---
553
+ if local_entries:
554
+ # If no cloud info, show header from local entry
555
+ if not cloud_env:
556
+ entry = local_entries[0]
557
+ console.print(f"\n📦 [bold]{entry.name}[/bold]")
558
+ if entry.description:
559
+ console.print(f" [dim]{entry.description}[/dim]")
560
+
561
+ console.print(f"\n[bold]💻 Local Installation{'s' if len(local_entries) > 1 else ''}:[/bold]")
562
+
563
+ for entry in local_entries:
564
+ kernel_status = "✓ kernel" if entry.has_kernel else "○ no kernel"
565
+ env_type = entry.type.value if hasattr(entry.type, "value") else str(entry.type)
566
+
567
+ console.print(f"\n [bold]env_id:[/bold] {entry.env_id}")
568
+ console.print(f" Path: {entry.path}")
569
+ console.print(f" Type: {env_type}")
570
+ if entry.python_version:
571
+ console.print(f" Python: {entry.python_version}")
572
+ if entry.python_executable:
573
+ console.print(f" Executable: {entry.python_executable}")
574
+ if entry.platform:
575
+ console.print(f" Platform: {entry.platform}")
576
+ if entry.kernel_name:
577
+ console.print(f" Kernel: {entry.kernel_name} [{kernel_status}]")
578
+ else:
579
+ console.print(f" Kernel: [{kernel_status}]")
580
+ if entry.shell_prompt:
581
+ console.print(f" Shell Prompt: {entry.shell_prompt}")
582
+ if entry.installed_at:
583
+ console.print(f" Installed: {entry.installed_at}")
584
+ elif entry.registered_at:
585
+ console.print(f" Registered: {entry.registered_at}")
586
+ if entry.slug:
587
+ console.print(f" Slug: {entry.slug}")
588
+ elif cloud_env:
589
+ console.print(f"\n[bold]💻 Local Installation:[/bold]")
590
+ console.print(f" [dim]Not installed locally[/dim]")
591
+
592
+ # --- Show Actions ---
593
+ console.print(f"\n[bold]Actions:[/bold]")
594
+
595
+ if cloud_env:
596
+ available = cloud_env.get("available_for_installation", False)
597
+ visibility = cloud_env.get("visibility", "private")
598
+
599
+ if available and not local_entries:
600
+ console.print(f" Install: [bold]qbraid envs install {slug_to_query}[/bold]")
601
+ if visibility == "private":
602
+ console.print(f" Share: [bold]qbraid envs share {slug_to_query} <email>[/bold]")
603
+ console.print(f" Publish: [bold]qbraid envs publish {slug_to_query}[/bold]")
604
+
605
+ if local_entries:
606
+ entry = local_entries[0]
607
+ if not entry.slug:
608
+ # Local-only environment - can be uploaded
609
+ console.print(f" Upload: [bold]qbraid envs upload {entry.env_id}[/bold]")
610
+ if not entry.has_kernel:
611
+ console.print(f" Add kernel: [bold]qbraid kernels add {entry.env_id}[/bold]")
612
+ console.print(f" Activate: [bold]qbraid envs activate {entry.env_id}[/bold]")
613
+ console.print(f" Remove: [bold]qbraid envs remove {entry.env_id}[/bold]")
614
+
615
+
443
616
  @envs_app.command(name="install")
444
617
  def envs_install(
445
618
  env_slug: str = typer.Argument(..., help="Environment slug to install (from 'qbraid envs available')"),
@@ -572,130 +745,603 @@ def envs_install(
572
745
  raise typer.Exit(1)
573
746
 
574
747
 
575
- @envs_app.command(name="publish")
576
- def envs_publish(
577
- slug: str = typer.Argument(..., help="Environment slug identifier"),
578
- path: Optional[Path] = typer.Option(
579
- None,
580
- "--path",
581
- "-p",
582
- help="Path to environment directory (default: lookup from registry)"
748
+ @envs_app.command(name="upload")
749
+ def envs_upload(
750
+ identifier: str = typer.Argument(
751
+ ..., help="Environment name (if unique) or env_id to upload"
583
752
  ),
584
753
  overwrite: bool = typer.Option(
585
754
  False,
586
755
  "--overwrite",
587
756
  "-o",
588
- help="Overwrite existing published environment"
757
+ help="Overwrite existing uploaded environment"
589
758
  ),
590
759
  ):
591
760
  """
592
- Publish environment to qBraid cloud storage for global distribution.
761
+ Upload environment to qBraid cloud storage (private by default).
593
762
 
594
- This command packages and uploads your environment to make it available
595
- for installation by other users via 'qbraid envs install'.
763
+ This uploads your local environment to make it available for sharing
764
+ with specific users. Use 'qbraid envs publish' after upload to request
765
+ public visibility.
596
766
 
597
767
  Examples:
598
- $ qbraid envs publish my_custom_env
599
- $ qbraid envs publish my_env --path ~/my_environment
600
- $ qbraid envs publish my_env --overwrite
768
+ $ qbraid envs upload my_custom_env
769
+ $ qbraid envs upload a1b2 --overwrite
601
770
  """
602
- from rich.console import Console
603
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
771
+ import asyncio
604
772
  from qbraid_core.services.environments.client import EnvironmentManagerClient
605
- from qbraid_core.services.environments.registry import EnvironmentRegistryManager
606
773
 
607
774
  console = Console()
608
775
 
609
- # Determine environment path
610
- if path:
611
- env_path = path.expanduser().resolve()
612
- if not env_path.exists():
613
- console.print(f"[red]❌ Error:[/red] Path does not exist: {env_path}")
614
- raise typer.Exit(1)
615
- else:
616
- # Look up from registry by slug
617
- try:
618
- registry_mgr = EnvironmentRegistryManager()
619
- found = registry_mgr.find_by_slug(slug)
620
- if not found:
621
- console.print(f"[red]❌ Error:[/red] Environment with slug '{slug}' not found in registry")
622
- console.print("\n[yellow]💡 Tip:[/yellow] Use --path to specify environment directory manually.")
623
- raise typer.Exit(1)
624
- env_id, env = found
625
- env_path = Path(env.path)
626
- except Exception as err:
627
- console.print(f"[red]❌ Error:[/red] Failed to locate environment: {err}")
628
- console.print("\n[yellow]💡 Tip:[/yellow] Use --path to specify environment directory manually.")
629
- raise typer.Exit(1)
630
-
631
- console.print(f"🚀 Publishing environment: [bold]{slug}[/bold]")
632
- console.print(f"📂 Source: {env_path}\n")
633
-
634
- # Define async function
635
- async def publish_environment_async():
636
- """Async wrapper for environment publishing."""
776
+ async def upload_environment_async():
777
+ """Async wrapper for environment upload."""
637
778
  client = EnvironmentManagerClient()
638
779
 
639
- # Progress tracking
640
- stages = {"archive": False, "upload": False}
641
-
642
- def progress_callback(stage: str, completed: int, total: int):
643
- """Track progress across stages."""
644
- stages[stage] = True
645
-
646
- with Progress(
780
+ progress_columns = (
647
781
  SpinnerColumn(),
648
- TextColumn("[progress.description]{task.description}"),
782
+ TextColumn("{task.description}"),
649
783
  BarColumn(),
650
784
  TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
651
- console=console,
652
- ) as progress:
653
- # Add tasks
654
- archive_task = progress.add_task("📦 Creating archive...", total=100)
655
- upload_task = progress.add_task("☁️ Uploading to cloud...", total=100)
656
-
657
- try:
658
- # TODO: Update this when env_share_publish branch is merged
659
- # This will use remote_publish_environment and handle directory renaming
660
- raise NotImplementedError(
661
- "Publish command will be updated when env_share_publish branch is merged. "
662
- "It will use remote_publish_environment to get slug from API, "
663
- "rename directory from env_id to slug, update paths, and upload."
664
- )
665
- # result = await client.publish_environment(
666
- # slug=slug,
667
- # env_path=str(env_path),
668
- # overwrite=overwrite,
669
- # progress_callback=progress_callback
670
- # )
785
+ TimeElapsedColumn(),
786
+ )
671
787
 
672
- # Update progress bars
673
- if stages.get("archive"):
674
- progress.update(archive_task, completed=100)
675
- if stages.get("upload"):
676
- progress.update(upload_task, completed=100)
788
+ with Progress(*progress_columns, console=console, transient=False) as progress:
789
+ tasks: dict[str, int] = {}
790
+
791
+ def get_task(stage: str, description: str, total: Optional[int]) -> int:
792
+ task_id = tasks.get(stage)
793
+ if task_id is None:
794
+ task_total = total if total and total > 0 else 100
795
+ task_id = progress.add_task(description, total=task_total)
796
+ tasks[stage] = task_id
797
+ return task_id
798
+
799
+ def progress_callback(stage: str, completed: int, total: int) -> None:
800
+ total_value = total if total and total > 0 else 100
801
+ if stage == "prepare":
802
+ task_id = get_task("prepare", "📋 Preparing environment...", total_value)
803
+ progress.update(task_id, completed=completed, total=total_value)
804
+ elif stage == "archive":
805
+ task_id = get_task("archive", "📦 Creating archive...", total_value)
806
+ progress.update(task_id, completed=completed, total=total_value)
807
+ elif stage == "upload":
808
+ task_id = get_task("upload", "☁️ Uploading to cloud...", total_value)
809
+ progress.update(task_id, completed=completed, total=total_value)
810
+
811
+ result = await client.upload_environment(
812
+ identifier=identifier,
813
+ overwrite=overwrite,
814
+ progress_callback=progress_callback,
815
+ )
677
816
 
678
- return result
817
+ return result
679
818
 
680
- except Exception as err:
681
- raise err
819
+ console.print(f"🚀 Uploading environment: [bold]{identifier}[/bold]\n")
682
820
 
683
- # Run async function
684
- import asyncio
685
821
  try:
686
- result = asyncio.run(publish_environment_async())
822
+ result = asyncio.run(upload_environment_async())
823
+
824
+ console.print(f"\n[green]✅ Upload completed![/green]")
825
+ console.print(f"📍 Slug: [bold]{result.get('slug', 'N/A')}[/bold]")
826
+ console.print(f"🆔 Env ID: {result.get('env_id', 'N/A')}")
827
+ if result.get('bucket'):
828
+ console.print(f"☁️ Bucket: {result.get('bucket')}")
687
829
 
688
- console.print(f"\n[green]✅ Published successfully![/green]")
689
- console.print(f"📍 Bucket: {result.get('bucket', 'N/A')}")
690
- console.print(f"📄 Path: {result.get('path', 'N/A')}")
691
- console.print(f"\n🎯 Users can now install with:")
692
- console.print(f" [bold cyan]qbraid envs install {slug}[/bold cyan]")
830
+ console.print(f"\n🎯 Next steps:")
831
+ console.print(f" • Share privately: [bold]qbraid envs share {result.get('slug')} <email>[/bold]")
832
+ console.print(f" • Request public: [bold]qbraid envs publish {result.get('slug')}[/bold]")
693
833
 
694
834
  except KeyboardInterrupt:
695
- console.print("\n[yellow]⚠️ Publishing cancelled by user[/yellow]")
835
+ console.print("\n[yellow]⚠️ Upload cancelled by user[/yellow]")
836
+ raise typer.Exit(1)
837
+ except Exception as err:
838
+ console.print(f"\n[red]❌ Upload failed:[/red] {err}")
839
+ raise typer.Exit(1)
840
+
841
+
842
+ @envs_app.command(name="publish")
843
+ def envs_publish(
844
+ slug: str = typer.Argument(..., help="Environment slug (from upload)"),
845
+ ):
846
+ """
847
+ Request to publish environment to the public catalog.
848
+
849
+ This submits your uploaded environment for admin review. Once approved,
850
+ it will be publicly visible and installable by all users.
851
+
852
+ The environment must be uploaded first with 'qbraid envs upload'.
853
+
854
+ Examples:
855
+ $ qbraid envs publish my_env_abc123
856
+ """
857
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
858
+
859
+ console = Console()
860
+
861
+ def request_publish():
862
+ client = EnvironmentManagerClient()
863
+ return client.request_publish(slug)
864
+
865
+ console.print(f"📤 Requesting to publish: [bold]{slug}[/bold]\n")
866
+
867
+ try:
868
+ result = run_progress_task(
869
+ request_publish,
870
+ description="Submitting publish request...",
871
+ error_message="Failed to submit publish request"
872
+ )
873
+
874
+ console.print(f"\n[green]✅ Publish request submitted![/green]")
875
+ console.print(f"📍 Slug: [bold]{slug}[/bold]")
876
+ console.print(f"📊 Status: [yellow]requested[/yellow]")
877
+ console.print(f"\n💡 Your request will be reviewed by qBraid admins.")
878
+ console.print(f" Check status with: [bold]qbraid envs publish-status {slug}[/bold]")
879
+
880
+ except Exception as err:
881
+ error_msg = str(err)
882
+ if "already" in error_msg.lower():
883
+ console.print(f"[yellow]⚠️ {error_msg}[/yellow]")
884
+ else:
885
+ console.print(f"[red]❌ Failed to request publish:[/red] {err}")
696
886
  raise typer.Exit(1)
887
+
888
+
889
+ @envs_app.command(name="publish-status")
890
+ def envs_publish_status(
891
+ slug: str = typer.Argument(..., help="Environment slug to check"),
892
+ ):
893
+ """
894
+ Check the publishing status of an environment.
895
+
896
+ Status values:
897
+ - none: Not submitted for publishing
898
+ - requested: Waiting for admin review
899
+ - pending: Under admin review
900
+ - approved: Published to public catalog
901
+ - denied: Publish request was denied
902
+
903
+ Examples:
904
+ $ qbraid envs publish-status my_env_abc123
905
+ """
906
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
907
+
908
+ console = Console()
909
+
910
+ def get_status():
911
+ client = EnvironmentManagerClient()
912
+ return client.get_publish_status(slug)
913
+
914
+ try:
915
+ status = run_progress_task(
916
+ get_status,
917
+ description="Checking publish status...",
918
+ error_message="Failed to get publish status"
919
+ )
920
+
921
+ # Color-code status
922
+ status_colors = {
923
+ "none": "dim",
924
+ "requested": "yellow",
925
+ "pending": "cyan",
926
+ "approved": "green",
927
+ "denied": "red",
928
+ }
929
+ color = status_colors.get(status, "white")
930
+
931
+ console.print(f"\n📊 Publish Status for [bold]{slug}[/bold]")
932
+ console.print(f" Status: [{color}]{status}[/{color}]")
933
+
934
+ # Show helpful next steps
935
+ if status == "none":
936
+ console.print(f"\n💡 To request publishing: [bold]qbraid envs publish {slug}[/bold]")
937
+ elif status == "requested":
938
+ console.print(f"\n⏳ Waiting for admin review...")
939
+ console.print(f" To cancel: [bold]qbraid envs publish-cancel {slug}[/bold]")
940
+ elif status == "pending":
941
+ console.print(f"\n🔍 Currently under admin review...")
942
+ elif status == "approved":
943
+ console.print(f"\n🎉 Environment is publicly available!")
944
+ console.print(f" Install with: [bold]qbraid envs install {slug}[/bold]")
945
+ elif status == "denied":
946
+ console.print(f"\n❌ Publish request was denied.")
947
+ console.print(f" To resubmit: [bold]qbraid envs publish {slug}[/bold]")
948
+
697
949
  except Exception as err:
698
- console.print(f"\n[red]❌ Publishing failed:[/red] {err}")
950
+ console.print(f"[red]❌ Failed to get status:[/red] {err}")
951
+ raise typer.Exit(1)
952
+
953
+
954
+ @envs_app.command(name="publish-cancel")
955
+ def envs_publish_cancel(
956
+ slug: str = typer.Argument(..., help="Environment slug"),
957
+ ):
958
+ """
959
+ Cancel a pending publish request.
960
+
961
+ This withdraws your publish request before it's approved.
962
+ You can resubmit later with 'qbraid envs publish'.
963
+
964
+ Examples:
965
+ $ qbraid envs publish-cancel my_env_abc123
966
+ """
967
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
968
+
969
+ console = Console()
970
+
971
+ def revoke_request():
972
+ client = EnvironmentManagerClient()
973
+ return client.revoke_publish_request(slug)
974
+
975
+ console.print(f"🚫 Cancelling publish request for: [bold]{slug}[/bold]\n")
976
+
977
+ try:
978
+ result = run_progress_task(
979
+ revoke_request,
980
+ description="Cancelling publish request...",
981
+ error_message="Failed to cancel publish request"
982
+ )
983
+
984
+ console.print(f"[green]✅ Publish request cancelled.[/green]")
985
+ console.print(f" Environment remains private/shared only.")
986
+ console.print(f"\n💡 To resubmit: [bold]qbraid envs publish {slug}[/bold]")
987
+
988
+ except Exception as err:
989
+ console.print(f"[red]❌ Failed to cancel:[/red] {err}")
990
+ raise typer.Exit(1)
991
+
992
+
993
+ @envs_app.command(name="publish-review")
994
+ def envs_publish_review(
995
+ slug: str = typer.Argument(..., help="Environment slug to review"),
996
+ ):
997
+ """
998
+ (Admin) Mark an environment as under review.
999
+
1000
+ This transitions the environment from 'requested' to 'pending' status,
1001
+ indicating an admin is actively reviewing it.
1002
+
1003
+ Examples:
1004
+ $ qbraid envs publish-review my_env_abc123
1005
+ """
1006
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
1007
+
1008
+ console = Console()
1009
+
1010
+ def set_pending():
1011
+ client = EnvironmentManagerClient()
1012
+ return client.set_publish_pending(slug)
1013
+
1014
+ console.print(f"🔍 Marking as under review: [bold]{slug}[/bold]\n")
1015
+
1016
+ try:
1017
+ result = run_progress_task(
1018
+ set_pending,
1019
+ description="Updating status to pending...",
1020
+ error_message="Failed to update status"
1021
+ )
1022
+
1023
+ console.print(f"[green]✅ Environment marked as under review.[/green]")
1024
+ console.print(f" Status: [cyan]pending[/cyan]")
1025
+ console.print(f"\n💡 Next steps:")
1026
+ console.print(f" • Approve: [bold]qbraid envs publish-approve {slug}[/bold]")
1027
+ console.print(f" • Deny: [bold]qbraid envs publish-deny {slug} --message \"reason\"[/bold]")
1028
+
1029
+ except Exception as err:
1030
+ error_msg = str(err)
1031
+ if "permission" in error_msg.lower() or "admin" in error_msg.lower():
1032
+ console.print(f"[red]❌ Admin access required.[/red]")
1033
+ else:
1034
+ console.print(f"[red]❌ Failed:[/red] {err}")
1035
+ raise typer.Exit(1)
1036
+
1037
+
1038
+ @envs_app.command(name="publish-approve")
1039
+ def envs_publish_approve(
1040
+ slug: str = typer.Argument(..., help="Environment slug to approve"),
1041
+ ):
1042
+ """
1043
+ (Admin) Approve and publish an environment to the public catalog.
1044
+
1045
+ This makes the environment publicly visible and installable by all users.
1046
+
1047
+ Examples:
1048
+ $ qbraid envs publish-approve my_env_abc123
1049
+ """
1050
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
1051
+
1052
+ console = Console()
1053
+
1054
+ def approve():
1055
+ client = EnvironmentManagerClient()
1056
+ return client.approve_publish(slug)
1057
+
1058
+ console.print(f"✅ Approving environment: [bold]{slug}[/bold]\n")
1059
+
1060
+ try:
1061
+ result = run_progress_task(
1062
+ approve,
1063
+ description="Approving and publishing...",
1064
+ error_message="Failed to approve"
1065
+ )
1066
+
1067
+ console.print(f"[green]✅ Environment published successfully![/green]")
1068
+ console.print(f" Slug: [bold]{slug}[/bold]")
1069
+ console.print(f" Visibility: [green]public[/green]")
1070
+ console.print(f"\n🎉 Users can now install with:")
1071
+ console.print(f" [bold]qbraid envs install {slug}[/bold]")
1072
+
1073
+ except Exception as err:
1074
+ error_msg = str(err)
1075
+ if "permission" in error_msg.lower() or "admin" in error_msg.lower():
1076
+ console.print(f"[red]❌ Admin access required.[/red]")
1077
+ else:
1078
+ console.print(f"[red]❌ Failed:[/red] {err}")
1079
+ raise typer.Exit(1)
1080
+
1081
+
1082
+ @envs_app.command(name="publish-deny")
1083
+ def envs_publish_deny(
1084
+ slug: str = typer.Argument(..., help="Environment slug to deny"),
1085
+ message: Optional[str] = typer.Option(
1086
+ None,
1087
+ "--message",
1088
+ "-m",
1089
+ help="Reason for denial (sent to owner)"
1090
+ ),
1091
+ ):
1092
+ """
1093
+ (Admin) Deny a publish request.
1094
+
1095
+ The owner will be notified with the denial reason.
1096
+
1097
+ Examples:
1098
+ $ qbraid envs publish-deny my_env_abc123
1099
+ $ qbraid envs publish-deny my_env_abc123 --message "Missing documentation"
1100
+ """
1101
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
1102
+
1103
+ console = Console()
1104
+
1105
+ def deny():
1106
+ client = EnvironmentManagerClient()
1107
+ return client.deny_publish(slug, message=message)
1108
+
1109
+ console.print(f"❌ Denying publish request: [bold]{slug}[/bold]")
1110
+ if message:
1111
+ console.print(f" Reason: {message}")
1112
+ console.print("")
1113
+
1114
+ try:
1115
+ result = run_progress_task(
1116
+ deny,
1117
+ description="Denying publish request...",
1118
+ error_message="Failed to deny"
1119
+ )
1120
+
1121
+ console.print(f"[yellow]✅ Publish request denied.[/yellow]")
1122
+ console.print(f" Owner has been notified.")
1123
+
1124
+ except Exception as err:
1125
+ error_msg = str(err)
1126
+ if "permission" in error_msg.lower() or "admin" in error_msg.lower():
1127
+ console.print(f"[red]❌ Admin access required.[/red]")
1128
+ else:
1129
+ console.print(f"[red]❌ Failed:[/red] {err}")
1130
+ raise typer.Exit(1)
1131
+
1132
+
1133
+ @envs_app.command(name="share")
1134
+ def envs_share(
1135
+ slug: str = typer.Argument(..., help="Environment slug to share"),
1136
+ email: str = typer.Argument(..., help="Email of user to share with"),
1137
+ read: bool = typer.Option(
1138
+ True,
1139
+ "--read/--no-read",
1140
+ help="Grant read permission (view environment)"
1141
+ ),
1142
+ write: bool = typer.Option(
1143
+ False,
1144
+ "--write",
1145
+ help="Grant write permission (modify environment)"
1146
+ ),
1147
+ execute: bool = typer.Option(
1148
+ True,
1149
+ "--execute/--no-execute",
1150
+ help="Grant execute permission (install environment)"
1151
+ ),
1152
+ ):
1153
+ """
1154
+ Share an environment with another user.
1155
+
1156
+ By default, grants read and execute permissions (user can view and install).
1157
+ Use --write to also allow modifications.
1158
+
1159
+ The environment must be uploaded first with 'qbraid envs upload'.
1160
+
1161
+ Examples:
1162
+ $ qbraid envs share my_env_abc123 colleague@example.com
1163
+ $ qbraid envs share my_env_abc123 collaborator@example.com --write
1164
+ $ qbraid envs share my_env_abc123 viewer@example.com --no-execute
1165
+ """
1166
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
1167
+
1168
+ console = Console()
1169
+
1170
+ # Build permissions list
1171
+ permissions = []
1172
+ if read:
1173
+ permissions.append("read")
1174
+ if write:
1175
+ permissions.append("write")
1176
+ if execute:
1177
+ permissions.append("execute")
1178
+
1179
+ if not permissions:
1180
+ console.print("[red]❌ Error:[/red] At least one permission must be granted.")
1181
+ raise typer.Exit(1)
1182
+
1183
+ def share():
1184
+ client = EnvironmentManagerClient()
1185
+ return client.share_environment(
1186
+ slug=slug,
1187
+ target_type="user",
1188
+ email=email,
1189
+ permissions=permissions
1190
+ )
1191
+
1192
+ console.print(f"🔗 Sharing environment: [bold]{slug}[/bold]")
1193
+ console.print(f" With: {email}")
1194
+ console.print(f" Permissions: {', '.join(permissions)}\n")
1195
+
1196
+ try:
1197
+ result = run_progress_task(
1198
+ share,
1199
+ description="Sharing environment...",
1200
+ error_message="Failed to share environment"
1201
+ )
1202
+
1203
+ console.print(f"[green]✅ Environment shared successfully![/green]")
1204
+ console.print(f" {email} can now access [bold]{slug}[/bold]")
1205
+
1206
+ if execute:
1207
+ console.print(f"\n💡 They can install with: [bold]qbraid envs install {slug}[/bold]")
1208
+
1209
+ except Exception as err:
1210
+ error_msg = str(err)
1211
+ if "not found" in error_msg.lower():
1212
+ console.print(f"[red]❌ Environment not found.[/red] Make sure it's uploaded first.")
1213
+ elif "owner" in error_msg.lower():
1214
+ console.print(f"[red]❌ Only the owner can share this environment.[/red]")
1215
+ else:
1216
+ console.print(f"[red]❌ Failed to share:[/red] {err}")
1217
+ raise typer.Exit(1)
1218
+
1219
+
1220
+ @envs_app.command(name="unshare")
1221
+ def envs_unshare(
1222
+ slug: str = typer.Argument(..., help="Environment slug"),
1223
+ email: str = typer.Argument(..., help="Email of user to revoke access from"),
1224
+ ):
1225
+ """
1226
+ Revoke access to a shared environment.
1227
+
1228
+ This removes all permissions for the specified user.
1229
+
1230
+ Examples:
1231
+ $ qbraid envs unshare my_env_abc123 former_colleague@example.com
1232
+ """
1233
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
1234
+
1235
+ console = Console()
1236
+
1237
+ def revoke():
1238
+ client = EnvironmentManagerClient()
1239
+ # Revoke all permissions
1240
+ return client.revoke_environment_access(
1241
+ slug=slug,
1242
+ target_type="user",
1243
+ email=email,
1244
+ permissions=["read", "write", "execute"]
1245
+ )
1246
+
1247
+ console.print(f"🚫 Revoking access to: [bold]{slug}[/bold]")
1248
+ console.print(f" From: {email}\n")
1249
+
1250
+ try:
1251
+ result = run_progress_task(
1252
+ revoke,
1253
+ description="Revoking access...",
1254
+ error_message="Failed to revoke access"
1255
+ )
1256
+
1257
+ console.print(f"[green]✅ Access revoked.[/green]")
1258
+ console.print(f" {email} no longer has access to [bold]{slug}[/bold]")
1259
+
1260
+ except Exception as err:
1261
+ error_msg = str(err)
1262
+ if "not found" in error_msg.lower():
1263
+ console.print(f"[red]❌ Environment not found.[/red]")
1264
+ elif "owner" in error_msg.lower():
1265
+ console.print(f"[red]❌ Only the owner can modify sharing settings.[/red]")
1266
+ else:
1267
+ console.print(f"[red]❌ Failed to revoke:[/red] {err}")
1268
+ raise typer.Exit(1)
1269
+
1270
+
1271
+ @envs_app.command(name="shared")
1272
+ def envs_shared(
1273
+ slug: str = typer.Argument(..., help="Environment slug to check"),
1274
+ ):
1275
+ """
1276
+ List users who have access to an environment.
1277
+
1278
+ Shows all users with read or write access to the specified environment.
1279
+
1280
+ Examples:
1281
+ $ qbraid envs shared my_env_abc123
1282
+ """
1283
+ from qbraid_core.services.environments.client import EnvironmentManagerClient
1284
+ from rich.table import Table
1285
+
1286
+ console = Console()
1287
+
1288
+ def get_shared():
1289
+ client = EnvironmentManagerClient()
1290
+ return client.get_environment_shared_with(slug)
1291
+
1292
+ try:
1293
+ result = run_progress_task(
1294
+ get_shared,
1295
+ description="Fetching shared users...",
1296
+ error_message="Failed to get shared users"
1297
+ )
1298
+
1299
+ read_users = result.get("readAccessUsers", [])
1300
+ write_users = result.get("writeAccessUsers", [])
1301
+
1302
+ console.print(f"\n📋 Sharing info for [bold]{slug}[/bold]\n")
1303
+
1304
+ if not read_users and not write_users:
1305
+ console.print("[dim]This environment is not shared with anyone.[/dim]")
1306
+ console.print(f"\n💡 Share with: [bold]qbraid envs share {slug} <email>[/bold]")
1307
+ return
1308
+
1309
+ # Create table
1310
+ table = Table(show_header=True, header_style="bold")
1311
+ table.add_column("Email", style="cyan")
1312
+ table.add_column("Name", style="dim")
1313
+ table.add_column("Permissions", style="green")
1314
+
1315
+ # Track users we've already added (to avoid duplicates)
1316
+ seen_emails = set()
1317
+
1318
+ # Add write users first (they have more permissions)
1319
+ for user in write_users:
1320
+ email = user.get("email", "N/A")
1321
+ name = user.get("name", "")
1322
+ if email not in seen_emails:
1323
+ table.add_row(email, name, "read, write, execute")
1324
+ seen_emails.add(email)
1325
+
1326
+ # Add read-only users
1327
+ for user in read_users:
1328
+ email = user.get("email", "N/A")
1329
+ name = user.get("name", "")
1330
+ if email not in seen_emails:
1331
+ table.add_row(email, name, "read, execute")
1332
+ seen_emails.add(email)
1333
+
1334
+ console.print(table)
1335
+ console.print(f"\n[dim]Total: {len(seen_emails)} user(s)[/dim]")
1336
+
1337
+ except Exception as err:
1338
+ error_msg = str(err)
1339
+ if "not found" in error_msg.lower():
1340
+ console.print(f"[red]❌ Environment not found.[/red]")
1341
+ elif "owner" in error_msg.lower():
1342
+ console.print(f"[red]❌ Only the owner can view sharing settings.[/red]")
1343
+ else:
1344
+ console.print(f"[red]❌ Failed to get shared users:[/red] {err}")
699
1345
  raise typer.Exit(1)
700
1346
 
701
1347
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.12.0a2
3
+ Version: 0.12.0a3
4
4
  Summary: Command Line Interface for interacting with all parts of the qBraid platform.
5
5
  Author-email: qBraid Development Team <contact@qbraid.com>
6
6
  License-Expression: LicenseRef-Proprietary
File without changes
File without changes
File without changes