superset-showtime 0.4.9__py3-none-any.whl → 0.5.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.

Potentially problematic release.


This version of superset-showtime might be problematic. Click here for more details.

showtime/__init__.py CHANGED
@@ -4,7 +4,7 @@
4
4
  Circus tent emoji state tracking for Apache Superset ephemeral environments.
5
5
  """
6
6
 
7
- __version__ = "0.4.9"
7
+ __version__ = "0.5.0"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
showtime/cli.py CHANGED
@@ -319,8 +319,10 @@ def list(
319
319
  # Create table with full terminal width
320
320
  table = Table(title="🎪 Environment List", expand=True)
321
321
  table.add_column("PR", style="cyan", min_width=6)
322
+ table.add_column("Type", style="white", min_width=8)
322
323
  table.add_column("Status", style="white", min_width=12)
323
324
  table.add_column("SHA", style="green", min_width=11)
325
+ table.add_column("Created", style="dim white", min_width=12)
324
326
  table.add_column("Superset URL", style="blue", min_width=25)
325
327
  table.add_column("AWS Logs", style="dim blue", min_width=15)
326
328
  table.add_column("TTL", style="yellow", min_width=6)
@@ -328,9 +330,23 @@ def list(
328
330
 
329
331
  status_emoji = STATUS_DISPLAY
330
332
 
331
- for env in sorted(filtered_envs, key=lambda e: e["pr_number"]):
333
+ # Sort by PR number, then by show type (active first, then building, then orphaned)
334
+ type_priority = {"active": 1, "building": 2, "orphaned": 3}
335
+ sorted_envs = sorted(filtered_envs, key=lambda e: (e["pr_number"], type_priority.get(e["show"].get("show_type", "orphaned"), 3)))
336
+
337
+ for env in sorted_envs:
332
338
  show_data = env["show"]
333
339
  pr_number = env["pr_number"]
340
+
341
+ # Show type with appropriate styling (using single-width chars for alignment)
342
+ show_type = show_data.get("show_type", "orphaned")
343
+ if show_type == "active":
344
+ type_display = "* active"
345
+ elif show_type == "building":
346
+ type_display = "# building"
347
+ else:
348
+ type_display = "! orphaned"
349
+
334
350
  # Make Superset URL clickable and show full URL
335
351
  if show_data["ip"]:
336
352
  full_url = f"http://{show_data['ip']}:8080"
@@ -348,10 +364,22 @@ def list(
348
364
  pr_url = f"https://github.com/apache/superset/pull/{pr_number}"
349
365
  clickable_pr = f"[link={pr_url}]{pr_number}[/link]"
350
366
 
367
+ # Format creation time for display
368
+ created_display = show_data.get("created_at", "-")
369
+ if created_display and created_display != "-":
370
+ # Convert 2025-08-25T05-18 to more readable format
371
+ try:
372
+ parts = created_display.replace("T", " ").replace("-", ":")
373
+ created_display = parts[-8:] # Show just HH:MM:SS
374
+ except:
375
+ pass # Keep original if parsing fails
376
+
351
377
  table.add_row(
352
378
  clickable_pr,
379
+ type_display,
353
380
  f"{status_emoji.get(show_data['status'], '❓')} {show_data['status']}",
354
381
  show_data["sha"],
382
+ created_display,
355
383
  superset_url,
356
384
  aws_logs_link,
357
385
  show_data["ttl"],
@@ -615,6 +643,102 @@ def setup_labels(
615
643
  p(f"🎪 [bold red]Error setting up labels:[/bold red] {e}")
616
644
 
617
645
 
646
+ @app.command()
647
+ def aws_cleanup(
648
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
649
+ force: bool = typer.Option(False, "--force", help="Delete all showtime AWS resources"),
650
+ ) -> None:
651
+ """🧹 Clean up orphaned AWS resources without GitHub labels"""
652
+ try:
653
+ from .core.aws import AWSInterface
654
+
655
+ aws = AWSInterface()
656
+
657
+ p("🔍 [bold blue]Scanning for orphaned AWS resources...[/bold blue]")
658
+
659
+ # 1. Get all GitHub PRs with circus labels
660
+ github_services = set()
661
+ try:
662
+ all_pr_numbers = PullRequest.find_all_with_environments()
663
+ p(f"📋 Found {len(all_pr_numbers)} PRs with circus labels:")
664
+
665
+ for pr_number in all_pr_numbers:
666
+ pr = PullRequest.from_id(pr_number)
667
+ p(f" 🎪 PR #{pr_number}: {len(pr.shows)} shows, {len(pr.circus_labels)} circus labels")
668
+
669
+ for show in pr.shows:
670
+ service_name = show.ecs_service_name
671
+ github_services.add(service_name)
672
+ p(f" 📝 Expected service: {service_name}")
673
+
674
+ # Show labels for debugging
675
+ if not pr.shows:
676
+ p(f" ⚠️ No shows found, labels: {pr.circus_labels[:3]}...") # First 3 labels
677
+
678
+ except Exception as e:
679
+ p(f"⚠️ GitHub scan failed: {e}")
680
+ github_services = set()
681
+
682
+ # 2. Get all AWS ECS services matching showtime pattern
683
+ p("\n☁️ [bold blue]Scanning AWS ECS services...[/bold blue]")
684
+ try:
685
+ aws_services = aws.find_showtime_services()
686
+ p(f"🔍 Found {len(aws_services)} AWS services with pr-* pattern")
687
+
688
+ for service in aws_services:
689
+ p(f" ☁️ AWS: {service}")
690
+ except Exception as e:
691
+ p(f"❌ AWS scan failed: {e}")
692
+ return
693
+
694
+ # 3. Find orphaned services
695
+ orphaned = [service for service in aws_services if service not in github_services]
696
+
697
+ if not orphaned:
698
+ p("\n✅ [bold green]No orphaned AWS resources found![/bold green]")
699
+ return
700
+
701
+ p(f"\n🚨 [bold red]Found {len(orphaned)} orphaned AWS resources:[/bold red]")
702
+ for service in orphaned:
703
+ p(f" 💰 {service} (consuming resources)")
704
+
705
+ if dry_run:
706
+ p(f"\n🎪 [bold yellow]DRY RUN[/bold yellow] - Would delete {len(orphaned)} services")
707
+ return
708
+
709
+ if not force:
710
+ confirm = typer.confirm(f"Delete {len(orphaned)} orphaned AWS services?")
711
+ if not confirm:
712
+ p("🎪 Cancelled")
713
+ return
714
+
715
+ # 4. Delete orphaned resources
716
+ deleted_count = 0
717
+ for service in orphaned:
718
+ p(f"🗑️ Deleting {service}...")
719
+ try:
720
+ # Extract PR number for delete_environment call
721
+ pr_match = service.replace("pr-", "").replace("-service", "")
722
+ parts = pr_match.split("-")
723
+ if len(parts) >= 2:
724
+ pr_number = int(parts[0])
725
+ success = aws.delete_environment(service.replace("-service", ""), pr_number)
726
+ if success:
727
+ p(f"✅ Deleted {service}")
728
+ deleted_count += 1
729
+ else:
730
+ p(f"❌ Failed to delete {service}")
731
+ else:
732
+ p(f"❌ Invalid service name format: {service}")
733
+ except Exception as e:
734
+ p(f"❌ Error deleting {service}: {e}")
735
+
736
+ p(f"\n🎪 ✅ Cleanup complete: deleted {deleted_count}/{len(orphaned)} services")
737
+
738
+ except Exception as e:
739
+ p(f"❌ AWS cleanup failed: {e}")
740
+
741
+
618
742
  @app.command()
619
743
  def cleanup(
620
744
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
showtime/core/aws.py CHANGED
@@ -670,6 +670,28 @@ class AWSInterface:
670
670
  print(f"❌ Failed to find expired services: {e}")
671
671
  return []
672
672
 
673
+ def find_showtime_services(self) -> List[str]:
674
+ """Find all ECS services managed by showtime (pr-* pattern)"""
675
+ try:
676
+ # List all services in cluster
677
+ response = self.ecs_client.list_services(cluster=self.cluster)
678
+
679
+ if not response.get("serviceArns"):
680
+ return []
681
+
682
+ # Extract service names and filter for showtime pattern
683
+ showtime_services = []
684
+ for service_arn in response["serviceArns"]:
685
+ service_name = service_arn.split("/")[-1] # Extract name from ARN
686
+ if service_name.startswith("pr-") and "-service" in service_name:
687
+ showtime_services.append(service_name)
688
+
689
+ return sorted(showtime_services)
690
+
691
+ except Exception as e:
692
+ print(f"❌ Failed to find showtime services: {e}")
693
+ return []
694
+
673
695
  def _find_pr_services(self, pr_number: int) -> List[Dict[str, Any]]:
674
696
  """Find all ECS services for a specific PR"""
675
697
  try:
@@ -356,55 +356,93 @@ class PullRequest:
356
356
  all_environments = []
357
357
  for pr_number in pr_numbers:
358
358
  pr = cls.from_id(pr_number)
359
- if pr.current_show:
360
- status = pr.get_status()
361
- status["pr_number"] = pr_number
362
- all_environments.append(status)
359
+ # Show ALL environments, not just current_show
360
+ for show in pr.shows:
361
+ # Determine show type based on pointer presence
362
+ show_type = "orphaned" # Default
363
+
364
+ # Check for active pointer
365
+ if any(label == f"🎪 🎯 {show.sha}" for label in pr.labels):
366
+ show_type = "active"
367
+ # Check for building pointer
368
+ elif any(label == f"🎪 🏗️ {show.sha}" for label in pr.labels):
369
+ show_type = "building"
370
+ # No pointer = orphaned
371
+
372
+ environment_data = {
373
+ "pr_number": pr_number,
374
+ "status": "active", # Keep for compatibility
375
+ "show": {
376
+ "sha": show.sha,
377
+ "status": show.status,
378
+ "ip": show.ip,
379
+ "ttl": show.ttl,
380
+ "requested_by": show.requested_by,
381
+ "created_at": show.created_at,
382
+ "aws_service_name": show.aws_service_name,
383
+ "show_type": show_type, # New field for display
384
+ },
385
+ }
386
+ all_environments.append(environment_data)
363
387
 
364
388
  return all_environments
365
389
 
366
390
  def _determine_action(self, target_sha: str) -> str:
367
- """Determine what sync action is needed"""
391
+ """Determine what sync action is needed based on target SHA state"""
392
+ target_sha_short = target_sha[:7] # Ensure we're working with short SHA
393
+
394
+ # Get the specific show for the target SHA
395
+ target_show = self.get_show_by_sha(target_sha_short)
396
+
368
397
  # Check for explicit trigger labels
369
398
  trigger_labels = [label for label in self.labels if "showtime-trigger-" in label]
370
399
 
371
400
  if trigger_labels:
372
401
  for trigger in trigger_labels:
373
402
  if "showtime-trigger-start" in trigger:
374
- if self.current_show and self.current_show.status == "failed":
375
- return "create_environment" # Replace failed environment
376
- elif self.current_show and self.current_show.needs_update(target_sha):
377
- return "rolling_update"
378
- elif self.current_show:
379
- return "no_action" # Same commit, healthy environment
403
+ if not target_show or target_show.status == "failed":
404
+ return "create_environment" # New SHA or failed SHA
405
+ elif target_show.status in ["building", "built", "deploying"]:
406
+ return "no_action" # Target SHA already in progress
407
+ elif target_show.status == "running":
408
+ return "create_environment" # Force rebuild with trigger
380
409
  else:
381
- return "create_environment"
410
+ return "create_environment" # Default for unknown states
382
411
  elif "showtime-trigger-stop" in trigger:
383
412
  return "destroy_environment"
384
413
 
385
- # No explicit triggers - check for auto-sync or creation
386
- if self.current_show and self.current_show.status != "failed" and self.current_show.needs_update(target_sha):
387
- return "auto_sync"
388
- elif not self.current_show or self.current_show.status == "failed":
389
- # No environment exists OR failed environment - allow creation without trigger (for CLI start)
414
+ # No explicit triggers - check target SHA state
415
+ if not target_show:
416
+ # Target SHA doesn't exist - create it
390
417
  return "create_environment"
418
+ elif target_show.status == "failed":
419
+ # Target SHA failed - rebuild it
420
+ return "create_environment"
421
+ elif target_show.status in ["building", "built", "deploying"]:
422
+ # Target SHA in progress - wait
423
+ return "no_action"
424
+ elif target_show.status == "running":
425
+ # Target SHA already running - no action needed
426
+ return "no_action"
391
427
 
392
428
  return "no_action"
393
429
 
394
430
  def _atomic_claim(self, target_sha: str, action: str, dry_run: bool = False) -> bool:
395
- """Atomically claim this PR for the current job"""
396
- # 1. Validate current state allows this action
431
+ """Atomically claim this PR for the current job based on target SHA state"""
432
+ target_sha_short = target_sha[:7]
433
+ target_show = self.get_show_by_sha(target_sha_short)
434
+
435
+ # 1. Validate current state allows this action for target SHA
397
436
  if action in ["create_environment", "rolling_update", "auto_sync"]:
398
- if self.current_show and self.current_show.status in [
437
+ if target_show and target_show.status in [
399
438
  "building",
400
439
  "built",
401
440
  "deploying",
402
441
  ]:
403
- return False # Another job active
442
+ return False # Target SHA already in progress
404
443
 
405
- # For rolling updates, running environments are OK to update
406
- if action in ["rolling_update", "auto_sync"] and self.current_show and self.current_show.status == "running":
407
- return True # Allow rolling updates on running environments
444
+ # Allow actions on failed, running, or non-existent target SHAs
445
+ return True
408
446
 
409
447
  if dry_run:
410
448
  print(f"🎪 [DRY-RUN] Would atomically claim PR for {action}")
showtime/core/show.py CHANGED
@@ -175,7 +175,7 @@ class Show:
175
175
  "--platform",
176
176
  "linux/amd64",
177
177
  "--target",
178
- "ci",
178
+ "dev",
179
179
  "--build-arg",
180
180
  "INCLUDE_CHROMIUM=false",
181
181
  "--build-arg",
@@ -270,8 +270,10 @@ class Show:
270
270
  elif emoji == "🤡": # User (clown!)
271
271
  show_data["requested_by"] = value
272
272
 
273
- # Only return Show if we found relevant labels for this SHA
274
- if any(label.endswith(f" {sha}") for label in labels if "🎯" in label or "🏗️" in label):
273
+ # Return Show if we found any status labels for this SHA
274
+ # For list purposes, we want to show ALL environments, even orphaned ones
275
+ has_status = any(label.startswith(f"🎪 {sha} 🚦 ") for label in labels)
276
+ if has_status:
275
277
  return cls(**show_data) # type: ignore[arg-type]
276
278
 
277
279
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.4.9
3
+ Version: 0.5.0
4
4
  Summary: 🎪 Apache Superset ephemeral environment management with circus tent emoji state tracking
5
5
  Project-URL: Homepage, https://github.com/apache/superset-showtime
6
6
  Project-URL: Documentation, https://superset-showtime.readthedocs.io/
@@ -0,0 +1,16 @@
1
+ showtime/__init__.py,sha256=KTG3yk6KrxuzGE2QA5xFEyUW9K8I4qRJhZUCCaYP1ww,448
2
+ showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
+ showtime/cli.py,sha256=mVGv9xe0ug2rHMtdRy8bETPeH1qwdrfTiIKE4U8opi8,30566
4
+ showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
5
+ showtime/core/aws.py,sha256=Pqj-vptEuahPKRV2C6aO66xArhhAIzUkcSSTa-t0r3g,33561
6
+ showtime/core/emojis.py,sha256=MHEDuPIdfNiop4zbNLuviz3eY05QiftYSHHCVbkfKhw,2129
7
+ showtime/core/github.py,sha256=uETvKDO2Yhpqg3fxLtrKaCuZR3b-1LVmgnf5aLcqrAQ,9988
8
+ showtime/core/github_messages.py,sha256=MfgwCukrEsWWesMsuL8saciDgP4nS-gijzu8DXr-Alg,7450
9
+ showtime/core/label_colors.py,sha256=efhbFnz_3nqEnEqmgyF6_hZbxtCu_fmb68BIIUpSsnk,3895
10
+ showtime/core/pull_request.py,sha256=HZa8_BgIEHCXvhhM3TfnzozAXije3fvi6XacfhPm11c,23498
11
+ showtime/core/show.py,sha256=CEG_f5TxbKuBqn8bg9K01_QJOlzWXTR4Cvh4YYgxZRk,9889
12
+ showtime/data/ecs-task-definition.json,sha256=2acmqoF-3CxaBJP_VDkMMpG_U2RI4VPk1JvFOprMFyc,2098
13
+ superset_showtime-0.5.0.dist-info/METADATA,sha256=y3IGzYt2fe1rtDUXYNuiJWoAHBCyj8Fh665txa1FvqU,12052
14
+ superset_showtime-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ superset_showtime-0.5.0.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
16
+ superset_showtime-0.5.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- showtime/__init__.py,sha256=lTJs8tOZFpuWaI6-1gqI-aev4BAX-KGCMG3TPglSKxQ,448
2
- showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
- showtime/cli.py,sha256=faFM6pe3gz49_1KrzUeri7dQffqz4WP92JmGxPaIOC0,25249
4
- showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
5
- showtime/core/aws.py,sha256=REeZ6_1C9f6mBchBAGa1MeDJeZIwir4IJ92HLRcK5ok,32636
6
- showtime/core/emojis.py,sha256=MHEDuPIdfNiop4zbNLuviz3eY05QiftYSHHCVbkfKhw,2129
7
- showtime/core/github.py,sha256=uETvKDO2Yhpqg3fxLtrKaCuZR3b-1LVmgnf5aLcqrAQ,9988
8
- showtime/core/github_messages.py,sha256=MfgwCukrEsWWesMsuL8saciDgP4nS-gijzu8DXr-Alg,7450
9
- showtime/core/label_colors.py,sha256=efhbFnz_3nqEnEqmgyF6_hZbxtCu_fmb68BIIUpSsnk,3895
10
- showtime/core/pull_request.py,sha256=EMeceF2NgqNyL_cCNv4345_2Cb6l0cAXb8lNV9pu8hc,21924
11
- showtime/core/show.py,sha256=-nMRShKWTjXGVuxuxrc0WK6l8ON-8iYm5QA8uvGoMOk,9806
12
- showtime/data/ecs-task-definition.json,sha256=2acmqoF-3CxaBJP_VDkMMpG_U2RI4VPk1JvFOprMFyc,2098
13
- superset_showtime-0.4.9.dist-info/METADATA,sha256=a-Mvygwwxhx3DLMVUUpFi8np70nF6RR1VSj-1fB-VLQ,12052
14
- superset_showtime-0.4.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- superset_showtime-0.4.9.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
16
- superset_showtime-0.4.9.dist-info/RECORD,,