superset-showtime 0.5.11__tar.gz → 0.5.12__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.

Potentially problematic release.


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

Files changed (34) hide show
  1. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/CLAUDE.md +4 -4
  2. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/PKG-INFO +1 -1
  3. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/__init__.py +1 -1
  4. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/git_validation.py +3 -0
  5. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/github.py +0 -8
  6. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/pull_request.py +98 -60
  7. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/data/ecs-task-definition.json +1 -1
  8. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/.claude/settings.local.json +0 -0
  9. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/.gitignore +0 -0
  10. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/.pre-commit-config.yaml +0 -0
  11. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/Makefile +0 -0
  12. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/README.md +0 -0
  13. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/dev-setup.sh +0 -0
  14. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/pypi-push.sh +0 -0
  15. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/pyproject.toml +0 -0
  16. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/requirements-dev.txt +0 -0
  17. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/requirements.txt +0 -0
  18. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/__main__.py +0 -0
  19. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/cli.py +0 -0
  20. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/__init__.py +0 -0
  21. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/aws.py +0 -0
  22. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/emojis.py +0 -0
  23. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/github_messages.py +0 -0
  24. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/label_colors.py +0 -0
  25. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/showtime/core/show.py +0 -0
  26. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/tests/__init__.py +0 -0
  27. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/tests/unit/__init__.py +0 -0
  28. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/tests/unit/test_label_transitions.py +0 -0
  29. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/tests/unit/test_pull_request.py +0 -0
  30. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/tests/unit/test_sha_specific_logic.py +0 -0
  31. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/tests/unit/test_show.py +0 -0
  32. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/uv.lock +0 -0
  33. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/workflows-reference/showtime-cleanup.yml +0 -0
  34. {superset_showtime-0.5.11 → superset_showtime-0.5.12}/workflows-reference/showtime-trigger.yml +0 -0
@@ -177,7 +177,7 @@ Double triggers can create race conditions in two scenarios:
177
177
  ### Current Atomic Claim Mechanism
178
178
 
179
179
  The `PullRequest._atomic_claim()` method handles basic conflicts by:
180
- 1. Checking if target SHA is already in progress states (`building`, `built`, `deploying`)
180
+ 1. Checking if target SHA is already in progress states (`building`, `built`, `deploying`)
181
181
  2. Removing trigger labels atomically
182
182
  3. Setting building state immediately
183
183
 
@@ -198,7 +198,7 @@ def can_start_job(self, target_sha: str, action: str, use_cached: bool = True) -
198
198
  # Returns (can_start, reason)
199
199
  ```
200
200
 
201
- #### Phase 2: Recovery Path (5% of calls, ~500ms)
201
+ #### Phase 2: Recovery Path (5% of calls, ~500ms)
202
202
  ```python
203
203
  def double_check_and_cleanup_stale_locks(self, target_sha: str, stale_hours: int = 1, dry_run: bool = False) -> bool:
204
204
  """Expensive: refresh labels, detect stale locks (>1h), clean them up"""
@@ -211,11 +211,11 @@ def double_check_and_cleanup_stale_locks(self, target_sha: str, stale_hours: int
211
211
  def _atomic_claim(self, target_sha: str, action: str, dry_run: bool = False) -> bool:
212
212
  # 1. Fast check with cached labels
213
213
  can_start, reason = self.can_start_job(target_sha, action, use_cached=True)
214
-
214
+
215
215
  if not can_start:
216
216
  # 2. Expensive double-check and cleanup
217
217
  can_start = self.double_check_and_cleanup_stale_locks(target_sha, stale_hours=1, dry_run=dry_run)
218
-
218
+
219
219
  # 3. Continue with existing trigger removal + building setup
220
220
  ```
221
221
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.5.11
3
+ Version: 0.5.12
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/
@@ -4,7 +4,7 @@
4
4
  Circus tent emoji state tracking for Apache Superset ephemeral environments.
5
5
  """
6
6
 
7
- __version__ = "0.5.11"
7
+ __version__ = "0.5.12"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
@@ -134,6 +134,9 @@ def _validate_sha_via_github_api(required_sha: str) -> Tuple[bool, Optional[str]
134
134
  # If status is 'ahead' or 'identical', required SHA is ancestor (good)
135
135
  # If status is 'behind', current is behind required (bad)
136
136
  if status in ["ahead", "identical"]:
137
+ print(
138
+ f"✅ Validated that required SHA {required_sha[:7]} is included in current branch"
139
+ )
137
140
  return True, None
138
141
  else:
139
142
  return (
@@ -111,14 +111,6 @@ class GitHubInterface:
111
111
  if response.status_code not in (200, 204, 404):
112
112
  response.raise_for_status()
113
113
 
114
- def set_labels(self, pr_number: int, labels: List[str]) -> None:
115
- """Replace all labels on a PR"""
116
- url = f"{self.base_url}/repos/{self.org}/{self.repo}/issues/{pr_number}/labels"
117
-
118
- with httpx.Client() as client:
119
- response = client.put(url, headers=self.headers, json={"labels": labels})
120
- response.raise_for_status()
121
-
122
114
  def get_latest_commit_sha(self, pr_number: int) -> str:
123
115
  """Get the latest commit SHA for a PR"""
124
116
  pr_data = self.get_pr_data(pr_number)
@@ -60,7 +60,7 @@ class PullRequest:
60
60
 
61
61
  def __init__(self, pr_number: int, labels: List[str]):
62
62
  self.pr_number = pr_number
63
- self.labels = labels
63
+ self.labels = set(labels) # Convert to set for O(1) operations
64
64
  self._shows = self._parse_shows_from_labels()
65
65
 
66
66
  @property
@@ -151,9 +151,40 @@ class PullRequest:
151
151
 
152
152
  def refresh_labels(self) -> None:
153
153
  """Refresh labels from GitHub and reparse shows"""
154
- self.labels = get_github().get_labels(self.pr_number)
154
+ self.labels = set(get_github().get_labels(self.pr_number))
155
155
  self._shows = self._parse_shows_from_labels()
156
156
 
157
+ def add_label(self, label: str) -> None:
158
+ """Add label with logging and optimistic state update"""
159
+ print(f"🏷️ Added: {label}")
160
+ get_github().add_label(self.pr_number, label)
161
+ self.labels.add(label)
162
+
163
+ def remove_label(self, label: str) -> None:
164
+ """Remove label with logging and optimistic state update"""
165
+ print(f"🗑️ Removed: {label}")
166
+ get_github().remove_label(self.pr_number, label)
167
+ self.labels.discard(label) # Safe - won't raise if not present
168
+
169
+ def remove_sha_labels(self, sha: str) -> None:
170
+ """Remove all labels for a specific SHA"""
171
+ sha_short = sha[:7]
172
+ labels_to_remove = [
173
+ label for label in self.labels if label.startswith("🎪") and sha_short in label
174
+ ]
175
+ if labels_to_remove:
176
+ print(f"🗑️ Removing SHA {sha_short} labels: {labels_to_remove}")
177
+ for label in labels_to_remove:
178
+ self.remove_label(label)
179
+
180
+ def remove_showtime_labels(self) -> None:
181
+ """Remove ALL circus tent labels"""
182
+ circus_labels = [label for label in self.labels if label.startswith("🎪 ")]
183
+ if circus_labels:
184
+ print(f"🎪 Removing all showtime labels: {circus_labels}")
185
+ for label in circus_labels:
186
+ self.remove_label(label)
187
+
157
188
  def analyze(self, target_sha: str, pr_state: str = "open") -> AnalysisResult:
158
189
  """Analyze what actions are needed (read-only, for --check-only)
159
190
 
@@ -245,10 +276,12 @@ class PullRequest:
245
276
  show.status = "running"
246
277
  print(f"✅ Deployment completed - environment running at {show.ip}:8080")
247
278
  self._update_show_labels(show, dry_run_github)
248
-
279
+
249
280
  # Blue-green cleanup: stop all other environments for this PR
250
- cleaned_count = self.stop_previous_environments(show.sha, dry_run_github, dry_run_aws)
251
-
281
+ cleaned_count = self.stop_previous_environments(
282
+ show.sha, dry_run_github, dry_run_aws
283
+ )
284
+
252
285
  # Show AWS console URLs for monitoring
253
286
  self._show_service_urls(show)
254
287
 
@@ -267,7 +300,7 @@ class PullRequest:
267
300
  print(f"🔄 Rolling update: {old_show.sha} → {new_show.sha}")
268
301
  self._post_rolling_start_comment(old_show, new_show, dry_run_github)
269
302
 
270
- # Phase 1: Docker build
303
+ # Phase 1: Docker build
271
304
  print("🐳 Building updated Docker image...")
272
305
  new_show.build_docker(dry_run_docker)
273
306
  new_show.status = "built"
@@ -280,10 +313,12 @@ class PullRequest:
280
313
  new_show.status = "running"
281
314
  print(f"✅ Rolling update completed - new environment at {new_show.ip}:8080")
282
315
  self._update_show_labels(new_show, dry_run_github)
283
-
316
+
284
317
  # Blue-green cleanup: stop all other environments for this PR
285
- cleaned_count = self.stop_previous_environments(new_show.sha, dry_run_github, dry_run_aws)
286
-
318
+ cleaned_count = self.stop_previous_environments(
319
+ new_show.sha, dry_run_github, dry_run_aws
320
+ )
321
+
287
322
  # Show AWS console URLs for monitoring
288
323
  self._show_service_urls(new_show)
289
324
 
@@ -298,7 +333,7 @@ class PullRequest:
298
333
  self._post_cleanup_comment(self.current_show, dry_run_github)
299
334
  # Remove all circus labels after successful stop
300
335
  if not dry_run_github:
301
- get_github().remove_circus_labels(self.pr_number)
336
+ self.remove_showtime_labels()
302
337
  print("🏷️ GitHub labels cleaned up")
303
338
  print("✅ Environment destroyed")
304
339
  return SyncResult(success=True, action_taken="destroy_environment")
@@ -330,7 +365,7 @@ class PullRequest:
330
365
  self.current_show.stop(**kwargs)
331
366
  # Remove all circus labels after successful stop
332
367
  if not kwargs.get("dry_run_github", False):
333
- get_github().remove_circus_labels(self.pr_number)
368
+ self.remove_showtime_labels()
334
369
  return SyncResult(success=True, action_taken="stopped")
335
370
  except Exception as e:
336
371
  return SyncResult(success=False, action_taken="stop_failed", error=str(e))
@@ -366,15 +401,15 @@ class PullRequest:
366
401
  for show in pr.shows:
367
402
  # Determine show type based on pointer presence
368
403
  show_type = "orphaned" # Default
369
-
404
+
370
405
  # Check for active pointer
371
406
  if any(label == f"🎪 🎯 {show.sha}" for label in pr.labels):
372
407
  show_type = "active"
373
- # Check for building pointer
408
+ # Check for building pointer
374
409
  elif any(label == f"🎪 🏗️ {show.sha}" for label in pr.labels):
375
410
  show_type = "building"
376
411
  # No pointer = orphaned
377
-
412
+
378
413
  environment_data = {
379
414
  "pr_number": pr_number,
380
415
  "status": "active", # Keep for compatibility
@@ -397,12 +432,12 @@ class PullRequest:
397
432
  """Determine what sync action is needed based on target SHA state"""
398
433
  # CRITICAL: Get fresh labels before any decisions
399
434
  self.refresh_labels()
400
-
435
+
401
436
  target_sha_short = target_sha[:7] # Ensure we're working with short SHA
402
-
437
+
403
438
  # Get the specific show for the target SHA
404
439
  target_show = self.get_show_by_sha(target_sha_short)
405
-
440
+
406
441
  # Check for explicit trigger labels
407
442
  trigger_labels = [label for label in self.labels if "showtime-trigger-" in label]
408
443
 
@@ -440,16 +475,16 @@ class PullRequest:
440
475
  """Atomically claim this PR for the current job based on target SHA state"""
441
476
  # CRITICAL: Get fresh labels before any decisions
442
477
  self.refresh_labels()
443
-
478
+
444
479
  target_sha_short = target_sha[:7]
445
480
  target_show = self.get_show_by_sha(target_sha_short)
446
-
447
- # 1. Validate current state allows this action for target SHA
481
+
482
+ # 1. Validate current state allows this action for target SHA
448
483
  if action in ["create_environment", "rolling_update", "auto_sync"]:
449
484
  if target_show and target_show.status in [
450
485
  "building",
451
- "built",
452
- "deploying",
486
+ "built",
487
+ "deploying",
453
488
  ]:
454
489
  return False # Target SHA already in progress - ONLY conflict case returns
455
490
 
@@ -462,7 +497,7 @@ class PullRequest:
462
497
  if trigger_labels:
463
498
  print(f"🏷️ Removing trigger labels: {trigger_labels}")
464
499
  for trigger_label in trigger_labels:
465
- get_github().remove_label(self.pr_number, trigger_label)
500
+ self.remove_label(trigger_label)
466
501
  else:
467
502
  print("🏷️ No trigger labels to remove")
468
503
 
@@ -470,16 +505,16 @@ class PullRequest:
470
505
  if action in ["create_environment", "rolling_update", "auto_sync"]:
471
506
  building_show = self._create_new_show(target_sha)
472
507
  building_show.status = "building"
473
-
474
- # Update labels to reflect building state
475
- print(f"🏷️ Removing existing circus labels...")
476
- get_github().remove_circus_labels(self.pr_number)
477
-
508
+
509
+ # Update labels to reflect building state - only remove for this SHA
510
+ print(f"🏷️ Removing labels for SHA {target_sha[:7]}...")
511
+ self.remove_sha_labels(target_sha)
512
+
478
513
  new_labels = building_show.to_circus_labels()
479
514
  print(f"🏷️ Creating new labels: {new_labels}")
480
515
  for label in new_labels:
481
516
  try:
482
- get_github().add_label(self.pr_number, label)
517
+ self.add_label(label)
483
518
  print(f" ✅ Added: {label}")
484
519
  except Exception as e:
485
520
  print(f" ❌ Failed to add {label}: {e}")
@@ -583,23 +618,21 @@ class PullRequest:
583
618
 
584
619
  # First, remove any existing status labels for this SHA to ensure clean transitions
585
620
  sha_status_labels = [
586
- label for label in self.labels
587
- if label.startswith(f"🎪 {show.sha} 🚦 ")
621
+ label for label in self.labels if label.startswith(f"🎪 {show.sha} 🚦 ")
588
622
  ]
589
623
  for old_status_label in sha_status_labels:
590
- get_github().remove_label(self.pr_number, old_status_label)
624
+ self.remove_label(old_status_label)
591
625
 
592
626
  # For running environments, ensure only ONE active pointer exists
593
627
  if show.status == "running":
594
628
  # Remove ALL existing active pointers (there should only be one)
595
629
  existing_active_pointers = [
596
- label for label in self.labels
597
- if label.startswith("🎪 🎯 ")
630
+ label for label in self.labels if label.startswith("🎪 🎯 ")
598
631
  ]
599
632
  for old_pointer in existing_active_pointers:
600
633
  print(f"🎯 Removing old active pointer: {old_pointer}")
601
- get_github().remove_label(self.pr_number, old_pointer)
602
-
634
+ self.remove_label(old_pointer)
635
+
603
636
  # CRITICAL: Refresh after removals before differential calculation
604
637
  if existing_active_pointers:
605
638
  print("🔄 Refreshing labels after pointer cleanup...")
@@ -607,11 +640,13 @@ class PullRequest:
607
640
 
608
641
  # Now do normal differential updates - only for this SHA
609
642
  current_sha_labels = {
610
- label for label in self.labels
611
- if label.startswith("🎪") and (
612
- label.startswith(f"🎪 {show.sha} ") or # SHA-first format: 🎪 abc123f 📅 ...
613
- label.startswith(f"🎪 🎯 {show.sha}") or # Pointer format: 🎪 🎯 abc123f
614
- label.startswith(f"🎪 🏗️ {show.sha}") # Building pointer: 🎪 🏗️ abc123f
643
+ label
644
+ for label in self.labels
645
+ if label.startswith("🎪")
646
+ and (
647
+ label.startswith(f"🎪 {show.sha} ") # SHA-first format: 🎪 abc123f 📅 ...
648
+ or label.startswith(f"🎪 🎯 {show.sha}") # Pointer format: 🎪 🎯 abc123f
649
+ or label.startswith(f"🎪 🏗️ {show.sha}") # Building pointer: 🎪 🏗️ abc123f
615
650
  )
616
651
  }
617
652
  desired_labels = set(show.to_circus_labels())
@@ -622,12 +657,12 @@ class PullRequest:
622
657
  # Only add labels that don't exist
623
658
  labels_to_add = desired_labels - current_sha_labels
624
659
  for label in labels_to_add:
625
- get_github().add_label(self.pr_number, label)
660
+ self.add_label(label)
626
661
 
627
662
  # Only remove labels that shouldn't exist (excluding status labels already handled)
628
663
  labels_to_remove = current_sha_labels - desired_labels
629
664
  for label in labels_to_remove:
630
- get_github().remove_label(self.pr_number, label)
665
+ self.remove_label(label)
631
666
 
632
667
  # Final refresh to update cache with all changes
633
668
  self.refresh_labels()
@@ -635,54 +670,57 @@ class PullRequest:
635
670
  def _show_service_urls(self, show: Show) -> None:
636
671
  """Show AWS console URLs for monitoring deployment"""
637
672
  from .github_messages import get_aws_console_urls
638
-
673
+
639
674
  urls = get_aws_console_urls(show.ecs_service_name)
640
- print(f"\n🎪 Monitor deployment progress:")
675
+ print("\n🎪 Monitor deployment progress:")
641
676
  print(f"📝 Logs: {urls['logs']}")
642
677
  print(f"📊 Service: {urls['service']}")
643
678
  print("")
644
679
 
645
- def stop_previous_environments(self, keep_sha: str, dry_run_github: bool = False, dry_run_aws: bool = False) -> int:
680
+ def stop_previous_environments(
681
+ self, keep_sha: str, dry_run_github: bool = False, dry_run_aws: bool = False
682
+ ) -> int:
646
683
  """Stop all environments except the specified SHA (blue-green cleanup)
647
-
684
+
648
685
  Args:
649
686
  keep_sha: SHA of environment to keep running
650
- dry_run_github: Skip GitHub label operations
687
+ dry_run_github: Skip GitHub label operations
651
688
  dry_run_aws: Skip AWS operations
652
-
689
+
653
690
  Returns:
654
691
  Number of environments stopped
655
692
  """
656
693
  # Note: Labels should be fresh from recent _update_show_labels() call
657
694
  stopped_count = 0
658
-
695
+
659
696
  for show in self.shows:
660
697
  if show.sha != keep_sha:
661
698
  print(f"🧹 Cleaning up old environment: {show.sha} ({show.status})")
662
699
  try:
663
700
  show.stop(dry_run_github=dry_run_github, dry_run_aws=dry_run_aws)
664
-
701
+
665
702
  # Remove ONLY existing labels for this old environment (not theoretical ones)
666
703
  if not dry_run_github:
667
704
  existing_labels = [
668
- label for label in self.labels
669
- if label.startswith(f"🎪 {show.sha} ") or
670
- label == f"🎪 🎯 {show.sha}" or
671
- label == f"🎪 🏗️ {show.sha}"
705
+ label
706
+ for label in self.labels
707
+ if label.startswith(f"🎪 {show.sha} ")
708
+ or label == f"🎪 🎯 {show.sha}"
709
+ or label == f"🎪 🏗️ {show.sha}"
672
710
  ]
673
711
  print(f"🏷️ Removing existing labels for {show.sha}: {existing_labels}")
674
712
  for label in existing_labels:
675
713
  try:
676
- get_github().remove_label(self.pr_number, label)
714
+ self.remove_label(label)
677
715
  except Exception as e:
678
716
  print(f"⚠️ Failed to remove label {label}: {e}")
679
-
717
+
680
718
  stopped_count += 1
681
719
  print(f"✅ Stopped environment {show.sha}")
682
-
720
+
683
721
  except Exception as e:
684
722
  print(f"❌ Failed to stop environment {show.sha}: {e}")
685
-
723
+
686
724
  if stopped_count > 0:
687
725
  print(f"🧹 Blue-green cleanup: stopped {stopped_count} old environments")
688
726
  # Refresh labels after cleanup
@@ -690,5 +728,5 @@ class PullRequest:
690
728
  self.refresh_labels()
691
729
  else:
692
730
  print("ℹ️ No old environments to clean up")
693
-
731
+
694
732
  return stopped_count
@@ -34,7 +34,7 @@
34
34
  },
35
35
  {
36
36
  "name": "SUPERSET__SQLALCHEMY_EXAMPLES_URI",
37
- "value": "duckdb:////app/data/examples.duckdb"
37
+ "value": "duckdb:////app/data/examples.duckdb?access_mode=read_only"
38
38
  },
39
39
  {
40
40
  "name": "SUPERSET_LOG_LEVEL",