superset-showtime 0.5.18__py3-none-any.whl → 0.6.3__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.5.18"
7
+ __version__ = "0.6.3"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
showtime/cli.py CHANGED
@@ -584,36 +584,6 @@ def sync(
584
584
  raise typer.Exit(1) from e
585
585
 
586
586
 
587
- @app.command()
588
- def handle_sync(pr_number: int) -> None:
589
- """🎪 Handle new commit sync (called by GitHub Actions on PR synchronize)"""
590
- try:
591
- pr = PullRequest.from_id(pr_number)
592
-
593
- # Only sync if there's an active environment
594
- if not pr.current_show:
595
- p(f"🎪 No active environment for PR #{pr_number} - skipping sync")
596
- return
597
-
598
- # Get latest commit SHA
599
- from .core.pull_request import get_github
600
-
601
- latest_sha = get_github().get_latest_commit_sha(pr_number)
602
-
603
- # Check if update is needed
604
- if not pr.current_show.needs_update(latest_sha):
605
- p(f"🎪 Environment already up to date for PR #{pr_number}")
606
- return
607
-
608
- p(f"🎪 Syncing PR #{pr_number} to commit {latest_sha[:7]}")
609
-
610
- # TODO: Implement rolling update logic
611
- p("🎪 [bold yellow]Sync logic not yet implemented[/bold yellow]")
612
-
613
- except Exception as e:
614
- p(f"🎪 [bold red]Error handling sync:[/bold red] {e}")
615
-
616
-
617
587
  @app.command()
618
588
  def setup_labels(
619
589
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what labels would be created"),
@@ -662,107 +632,10 @@ def setup_labels(
662
632
  p(f"🎪 [bold red]Error setting up labels:[/bold red] {e}")
663
633
 
664
634
 
665
- @app.command()
666
- def aws_cleanup(
667
- dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
668
- force: bool = typer.Option(False, "--force", help="Delete all showtime AWS resources"),
669
- ) -> None:
670
- """🧹 Clean up orphaned AWS resources without GitHub labels"""
671
- try:
672
- from .core.aws import AWSInterface
673
-
674
- aws = AWSInterface()
675
-
676
- p("🔍 [bold blue]Scanning for orphaned AWS resources...[/bold blue]")
677
-
678
- # 1. Get all GitHub PRs with circus labels
679
- github_services = set()
680
- try:
681
- all_pr_numbers = PullRequest.find_all_with_environments()
682
- p(f"📋 Found {len(all_pr_numbers)} PRs with circus labels:")
683
-
684
- for pr_number in all_pr_numbers:
685
- pr = PullRequest.from_id(pr_number)
686
- p(
687
- f" 🎪 PR #{pr_number}: {len(pr.shows)} shows, {len(pr.circus_labels)} circus labels"
688
- )
689
-
690
- for show in pr.shows:
691
- service_name = show.ecs_service_name
692
- github_services.add(service_name)
693
- p(f" 📝 Expected service: {service_name}")
694
-
695
- # Show labels for debugging
696
- if not pr.shows:
697
- p(f" ⚠️ No shows found, labels: {pr.circus_labels[:3]}...") # First 3 labels
698
-
699
- except Exception as e:
700
- p(f"⚠️ GitHub scan failed: {e}")
701
- github_services = set()
702
-
703
- # 2. Get all AWS ECS services matching showtime pattern
704
- p("\n☁️ [bold blue]Scanning AWS ECS services...[/bold blue]")
705
- try:
706
- aws_services = aws.find_showtime_services()
707
- p(f"🔍 Found {len(aws_services)} AWS services with pr-* pattern")
708
-
709
- for service in aws_services:
710
- p(f" ☁️ AWS: {service}")
711
- except Exception as e:
712
- p(f"❌ AWS scan failed: {e}")
713
- return
714
-
715
- # 3. Find orphaned services
716
- orphaned = [service for service in aws_services if service not in github_services]
717
-
718
- if not orphaned:
719
- p("\n✅ [bold green]No orphaned AWS resources found![/bold green]")
720
- return
721
-
722
- p(f"\n🚨 [bold red]Found {len(orphaned)} orphaned AWS resources:[/bold red]")
723
- for service in orphaned:
724
- p(f" 💰 {service} (consuming resources)")
725
-
726
- if dry_run:
727
- p(f"\n🎪 [bold yellow]DRY RUN[/bold yellow] - Would delete {len(orphaned)} services")
728
- return
729
-
730
- if not force:
731
- confirm = typer.confirm(f"Delete {len(orphaned)} orphaned AWS services?")
732
- if not confirm:
733
- p("🎪 Cancelled")
734
- return
735
-
736
- # 4. Delete orphaned resources
737
- deleted_count = 0
738
- for service in orphaned:
739
- p(f"🗑️ Deleting {service}...")
740
- try:
741
- # Extract PR number for delete_environment call
742
- pr_match = service.replace("pr-", "").replace("-service", "")
743
- parts = pr_match.split("-")
744
- if len(parts) >= 2:
745
- pr_number = int(parts[0])
746
- success = aws.delete_environment(service, pr_number)
747
- if success:
748
- p(f"✅ Deleted {service}")
749
- deleted_count += 1
750
- else:
751
- p(f"❌ Failed to delete {service}")
752
- else:
753
- p(f"❌ Invalid service name format: {service}")
754
- except Exception as e:
755
- p(f"❌ Error deleting {service}: {e}")
756
-
757
- p(f"\n🎪 ✅ Cleanup complete: deleted {deleted_count}/{len(orphaned)} services")
758
-
759
- except Exception as e:
760
- p(f"❌ AWS cleanup failed: {e}")
761
-
762
-
763
635
  @app.command()
764
636
  def cleanup(
765
637
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
638
+ force: bool = typer.Option(False, "--force", help="Skip interactive prompts"),
766
639
  older_than: str = typer.Option(
767
640
  "48h", "--older-than", help="Clean environments older than this (ignored if --respect-ttl)"
768
641
  ),
@@ -777,6 +650,11 @@ def cleanup(
777
650
  "--cleanup-labels/--no-cleanup-labels",
778
651
  help="Also cleanup SHA-based label definitions from repository",
779
652
  ),
653
+ cleanup_aws_orphans: bool = typer.Option(
654
+ True,
655
+ "--cleanup-aws-orphans/--no-cleanup-aws-orphans",
656
+ help="Also cleanup orphaned AWS resources",
657
+ ),
780
658
  ) -> None:
781
659
  """🎪 Clean up orphaned or expired environments and labels"""
782
660
  try:
@@ -811,6 +689,106 @@ def cleanup(
811
689
  else:
812
690
  p("🎪 No expired environments found")
813
691
 
692
+ # Phase 2: AWS orphan cleanup
693
+ aws_cleaned_count = 0
694
+ if cleanup_aws_orphans:
695
+ from .core.aws import AWSInterface
696
+
697
+ p("\n☁️ [bold blue]Scanning for orphaned AWS resources...[/bold blue]")
698
+ aws = AWSInterface()
699
+
700
+ try:
701
+ # Get expected services from GitHub
702
+ github_services = set()
703
+ for pr_number in pr_numbers:
704
+ pr = PullRequest.from_id(pr_number)
705
+ for show in pr.shows:
706
+ github_services.add(show.ecs_service_name)
707
+
708
+ # Find AWS orphans
709
+ aws_services = aws.list_circus_environments()
710
+ aws_orphans = [
711
+ svc for svc in aws_services if svc.get("service_name") not in github_services
712
+ ]
713
+
714
+ if aws_orphans:
715
+ p(f"☁️ Found {len(aws_orphans)} orphaned AWS resources:")
716
+ for orphan in aws_orphans[:3]:
717
+ p(f" • {orphan['service_name']}")
718
+ if len(aws_orphans) > 3:
719
+ p(f" ... and {len(aws_orphans) - 3} more")
720
+
721
+ if not force and not dry_run:
722
+ if typer.confirm(f"Delete {len(aws_orphans)} orphaned AWS resources?"):
723
+ # Clean up AWS orphans
724
+ for orphan in aws_orphans:
725
+ if not dry_run:
726
+ aws.delete_service(orphan["service_name"])
727
+ aws_cleaned_count += 1
728
+ else:
729
+ p("❌ Skipping AWS orphan cleanup")
730
+ elif force or dry_run:
731
+ aws_cleaned_count = len(aws_orphans)
732
+ if not dry_run:
733
+ for orphan in aws_orphans:
734
+ aws.delete_service(orphan["service_name"])
735
+
736
+ if aws_cleaned_count > 0:
737
+ p(f"☁️ ✅ Cleaned up {aws_cleaned_count} orphaned AWS resources")
738
+ else:
739
+ p("☁️ No orphaned AWS resources found")
740
+
741
+ except Exception as e:
742
+ p(f"⚠️ AWS orphan scan failed: {e}")
743
+
744
+ # Phase 3: Repository label cleanup
745
+ label_cleaned_count = 0
746
+ if cleanup_labels:
747
+ from .core.pull_request import get_github
748
+
749
+ p("\n🏷️ [bold blue]Scanning for orphaned repository labels...[/bold blue]")
750
+ github = get_github()
751
+
752
+ try:
753
+ orphaned_labels = github.find_orphaned_labels(dry_run=True) # Preview
754
+
755
+ if orphaned_labels:
756
+ p(f"🏷️ Found {len(orphaned_labels)} orphaned repository labels:")
757
+ for label in orphaned_labels[:3]:
758
+ p(f" • {label}")
759
+ if len(orphaned_labels) > 3:
760
+ p(f" ... and {len(orphaned_labels) - 3} more")
761
+
762
+ if not force and not dry_run:
763
+ if typer.confirm(
764
+ f"Delete {len(orphaned_labels)} orphaned labels from repository?"
765
+ ):
766
+ deleted_labels = github.find_orphaned_labels(dry_run=False)
767
+ label_cleaned_count = len(deleted_labels)
768
+ else:
769
+ p("❌ Skipping repository label cleanup")
770
+ elif force or dry_run:
771
+ label_cleaned_count = len(orphaned_labels)
772
+ if not dry_run:
773
+ github.find_orphaned_labels(dry_run=False)
774
+
775
+ if label_cleaned_count > 0:
776
+ p(f"🏷️ ✅ Cleaned up {label_cleaned_count} orphaned repository labels")
777
+ else:
778
+ p("🏷️ No orphaned repository labels found")
779
+
780
+ except Exception as e:
781
+ p(f"⚠️ Repository label scan failed: {e}")
782
+
783
+ # Final summary
784
+ total_cleaned = cleaned_count + aws_cleaned_count + label_cleaned_count
785
+ if total_cleaned > 0:
786
+ p(
787
+ f"\n🎉 [bold green]Total cleanup: {cleaned_count} environments + {aws_cleaned_count} AWS orphans + {label_cleaned_count} labels[/bold green]"
788
+ )
789
+ else:
790
+ p("\n✨ [bold green]No cleanup needed - everything is clean![/bold green]")
791
+
814
792
  except Exception as e:
815
793
  p(f"❌ Cleanup failed: {e}")
816
794
 
showtime/core/github.py CHANGED
@@ -143,9 +143,9 @@ class GitHubInterface:
143
143
  url = f"{self.base_url}/search/issues"
144
144
  # Search for PRs with any circus tent labels
145
145
  params = {
146
- "q": f"repo:{self.org}/{self.repo} is:pr 🎪",
146
+ "q": f"repo:{self.org}/{self.repo} is:pr is:open 🎪",
147
147
  "per_page": "100",
148
- } # Include closed PRs
148
+ } # Only open PRs - closed PRs should have cleaned up labels
149
149
 
150
150
  with httpx.Client() as client:
151
151
  response = client.get(url, headers=self.headers, params=params)
@@ -209,8 +209,8 @@ class GitHubInterface:
209
209
  all_labels = self.get_repository_labels()
210
210
  sha_labels = []
211
211
 
212
- # Find labels with SHA patterns (7+ hex chars after 🎪)
213
- sha_pattern = re.compile(r"^🎪 .* [a-f0-9]{7,}( .*)?$")
212
+ # Find labels with SHA patterns (7+ hex chars anywhere in label)
213
+ sha_pattern = re.compile(r"^🎪 .*[a-f0-9]{7,}.*$")
214
214
 
215
215
  for label in all_labels:
216
216
  if sha_pattern.match(label):
@@ -225,6 +225,69 @@ class GitHubInterface:
225
225
 
226
226
  return sha_labels
227
227
 
228
+ def find_orphaned_labels(self, dry_run: bool = False) -> List[str]:
229
+ """Find labels that exist in repository but aren't used on any PR"""
230
+ import re
231
+
232
+ print("🔍 Scanning repository labels...")
233
+
234
+ # 1. Get all repository labels with SHA patterns
235
+ all_repo_labels = self.get_repository_labels()
236
+ sha_pattern = re.compile(r"^🎪 .*[a-f0-9]{7,}.*$")
237
+ sha_repo_labels = {label for label in all_repo_labels if sha_pattern.match(label)}
238
+
239
+ print(f"📋 Found {len(sha_repo_labels)} SHA-containing labels in repository")
240
+
241
+ # 2. Get all labels actually used on PRs with circus labels
242
+ print("🔍 Scanning PRs with circus labels...")
243
+
244
+ # Import here to avoid circular import
245
+ import importlib
246
+
247
+ pull_request_module = importlib.import_module("showtime.core.pull_request")
248
+ PullRequest = pull_request_module.PullRequest
249
+
250
+ try:
251
+ pr_numbers = PullRequest.find_all_with_environments()
252
+ print(f"📋 Found {len(pr_numbers)} PRs with circus labels")
253
+
254
+ used_labels = set()
255
+ for pr_number in pr_numbers:
256
+ pr_labels = self.get_labels(pr_number)
257
+ circus_labels = {label for label in pr_labels if label.startswith("🎪 ")}
258
+ used_labels.update(circus_labels)
259
+
260
+ print(f"📋 Found {len(used_labels)} circus labels actually used on PRs")
261
+
262
+ # 3. Set difference to find orphaned labels
263
+ orphaned_labels = sha_repo_labels - used_labels
264
+
265
+ print(f"🗑️ Found {len(orphaned_labels)} truly orphaned labels")
266
+
267
+ # Debug: Show some examples if in dry run
268
+ if dry_run and orphaned_labels:
269
+ print("🔍 Examples of orphaned labels:")
270
+ for label in list(orphaned_labels)[:5]:
271
+ print(f" • {label}")
272
+ if dry_run and used_labels:
273
+ print("🔍 Examples of used labels:")
274
+ for label in list(used_labels)[:5]:
275
+ print(f" • {label}")
276
+
277
+ if not dry_run and orphaned_labels:
278
+ deleted_labels = []
279
+ for label in orphaned_labels:
280
+ if self.delete_repository_label(label):
281
+ deleted_labels.append(label)
282
+ return deleted_labels
283
+
284
+ return list(orphaned_labels)
285
+
286
+ except Exception as e:
287
+ print(f"⚠️ Error during orphan detection: {e}")
288
+ # Fallback to old pattern-based method
289
+ return self.cleanup_sha_labels(dry_run)
290
+
228
291
  def create_or_update_label(self, name: str, color: str, description: str) -> bool:
229
292
  """Create or update a label with color and description"""
230
293
  import urllib.parse
@@ -325,7 +325,12 @@ class PullRequest:
325
325
  )
326
326
 
327
327
  # 3. Atomic claim for environment changes (PR-level lock)
328
- if action_needed in ["create_environment", "rolling_update", "auto_sync"]:
328
+ if action_needed in [
329
+ "create_environment",
330
+ "rolling_update",
331
+ "auto_sync",
332
+ "destroy_environment",
333
+ ]:
329
334
  print(f"🔒 Claiming environment for {action_needed}...")
330
335
  if not self._atomic_claim(target_sha, action_needed, dry_run_github):
331
336
  print("❌ Claim failed - another job is active")
@@ -543,10 +548,14 @@ class PullRequest:
543
548
  elif "showtime-trigger-stop" in trigger:
544
549
  return "destroy_environment"
545
550
 
546
- # No explicit triggers - check target SHA state
551
+ # No explicit triggers - only auto-create if there's ANY previous environment
547
552
  if not target_show:
548
- # Target SHA doesn't exist - create it
549
- return "create_environment"
553
+ # Target SHA doesn't exist - only create if there's any previous environment
554
+ if self.shows: # Any previous environment exists
555
+ return "create_environment"
556
+ else:
557
+ # No previous environments - don't auto-create without explicit trigger
558
+ return "no_action"
550
559
  elif target_show.status == "failed":
551
560
  # Target SHA failed - rebuild it
552
561
  return "create_environment"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.5.18
3
+ Version: 0.6.3
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/
@@ -1,17 +1,17 @@
1
- showtime/__init__.py,sha256=apR60gqUgO9WuV06_c-B1cXzwzdQRcEaY-uYSPrrX0A,449
1
+ showtime/__init__.py,sha256=IDSoi3ERNMkK-VnFVyhCFJdw5YdzsIIUFwHUXKrSSDw,448
2
2
  showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
- showtime/cli.py,sha256=8vIJT5TiqXuHDGxRBg6jV3oNv5nKrmDOs5OgltycPeI,31664
3
+ showtime/cli.py,sha256=TLv9NaqPyewKJi9uCTZKWBijGelqunmsoSo7cyKajV4,31640
4
4
  showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
5
5
  showtime/core/aws.py,sha256=uTjJAvEBQMyTccS93WZeNPhfeKQhJgOQQ0BJdnQjvCU,35007
6
6
  showtime/core/emojis.py,sha256=arK0N5Q5FLkvOkci-lacb3WS56LTvY8NjYRqt_lhP9s,2188
7
7
  showtime/core/git_validation.py,sha256=3dmSGpMDplDAmKWHUyoUEPgt3__8oTuBZxbfuhocT00,6831
8
- showtime/core/github.py,sha256=gMPJ5TOT6DdZk4y0XqW-C69I7O8A4eI40TgT4IFPqhQ,9623
8
+ showtime/core/github.py,sha256=mSOqRLy2KMDhWUS37V2gJ-CQdeBpEqunBRKL10v5hxU,12268
9
9
  showtime/core/github_messages.py,sha256=MfgwCukrEsWWesMsuL8saciDgP4nS-gijzu8DXr-Alg,7450
10
10
  showtime/core/label_colors.py,sha256=gSe7EIMl4YjWkIgKHUvuaRSwgEB_B-NYQBxFFlF8Z3s,4065
11
- showtime/core/pull_request.py,sha256=r-4tCjEjsZOcTk4cjw57yyhNzq1sLPt4SYa1S7RcDlQ,31998
11
+ showtime/core/pull_request.py,sha256=v_1hi-UNOckIv1-C7JQWKyLgVna_6vflMZoRAymPxgE,32355
12
12
  showtime/core/show.py,sha256=sOgZvGXwdcNDsidF1F_XwPXlSeTb8-Zeqhqb8w1pqAM,9973
13
13
  showtime/data/ecs-task-definition.json,sha256=d-NLkIhvr4C6AnwDfDIwUTx-6KFMH9wRkt6pVCbqZY4,2365
14
- superset_showtime-0.5.18.dist-info/METADATA,sha256=aTfADzrQ7S-eVsJJDlOvIOaDeSJ4WHqdfW0MtKsqFpg,12053
15
- superset_showtime-0.5.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- superset_showtime-0.5.18.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
- superset_showtime-0.5.18.dist-info/RECORD,,
14
+ superset_showtime-0.6.3.dist-info/METADATA,sha256=mXRuwlkq93XYCc5EcoQpHG3jYTaeipyPGsfvGZ6vmFQ,12052
15
+ superset_showtime-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ superset_showtime-0.6.3.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
+ superset_showtime-0.6.3.dist-info/RECORD,,