superset-showtime 0.5.12__py3-none-any.whl → 0.5.18__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.12"
7
+ __version__ = "0.5.18"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
showtime/core/emojis.py CHANGED
@@ -13,6 +13,7 @@ EMOJI_MEANINGS = {
13
13
  "🚦": "status", # Traffic light for environment status
14
14
  "🏗️": "building", # Construction for building environments
15
15
  "🎯": "active", # Target for currently active environment
16
+ "🔒": "blocked", # Lock for blocking all operations
16
17
  # Metadata
17
18
  "📅": "created_at", # Calendar for creation timestamp
18
19
  "🌐": "ip", # Globe for IP address
@@ -31,6 +31,10 @@ LABEL_DEFINITIONS = {
31
31
  "color": "FFE4B5", # Light orange
32
32
  "description": "Freeze PR - prevent auto-sync on new commits",
33
33
  },
34
+ "🎪 🔒 showtime-blocked": {
35
+ "color": "dc3545", # Red - blocking/danger
36
+ "description": "Block all Showtime operations - maintenance mode",
37
+ },
34
38
  }
35
39
 
36
40
  # Status-specific label patterns (generated dynamically)
@@ -90,20 +90,10 @@ class PullRequest:
90
90
 
91
91
  @property
92
92
  def building_show(self) -> Optional[Show]:
93
- """The currently building show (from 🏗️ label)"""
94
- building_sha = None
95
- for label in self.labels:
96
- if label.startswith("🎪 🏗️ "):
97
- building_sha = label.split(" ")[2]
98
- break
99
-
100
- if not building_sha:
101
- return None
102
-
93
+ """The currently building show (from building/deploying status)"""
103
94
  for show in self.shows:
104
- if show.sha == building_sha:
95
+ if show.status in ["building", "deploying"]:
105
96
  return show
106
-
107
97
  return None
108
98
 
109
99
  @property
@@ -185,6 +175,88 @@ class PullRequest:
185
175
  for label in circus_labels:
186
176
  self.remove_label(label)
187
177
 
178
+ def set_show_status(self, show: Show, new_status: str) -> None:
179
+ """Atomically update show status with thorough label cleanup"""
180
+ show.status = new_status
181
+
182
+ # 1. Refresh labels to get current GitHub state
183
+ self.refresh_labels()
184
+
185
+ # 2. Remove ALL existing status labels for this SHA (not just the "expected" one)
186
+ status_labels_to_remove = [
187
+ label for label in self.labels if label.startswith(f"🎪 {show.sha} 🚦 ")
188
+ ]
189
+
190
+ for label in status_labels_to_remove:
191
+ self.remove_label(label)
192
+
193
+ # 3. Add the new status label
194
+ new_status_label = f"🎪 {show.sha} 🚦 {new_status}"
195
+ self.add_label(new_status_label)
196
+
197
+ def set_active_show(self, show: Show) -> None:
198
+ """Atomically set this show as the active environment"""
199
+ from .emojis import CIRCUS_PREFIX, MEANING_TO_EMOJI
200
+
201
+ # 1. Refresh to get current state
202
+ self.refresh_labels()
203
+
204
+ # 2. Remove ALL existing active pointers (ensure only one)
205
+ active_emoji = MEANING_TO_EMOJI["active"] # Gets 🎯
206
+ active_prefix = f"{CIRCUS_PREFIX} {active_emoji} " # "🎪 🎯 "
207
+ active_pointers = [label for label in self.labels if label.startswith(active_prefix)]
208
+
209
+ for pointer in active_pointers:
210
+ self.remove_label(pointer)
211
+
212
+ # 3. Set this show as the new active one
213
+ active_pointer = f"{active_prefix}{show.sha}" # "🎪 🎯 abc123f"
214
+ self.add_label(active_pointer)
215
+
216
+ def _check_authorization(self) -> bool:
217
+ """Check if current GitHub actor is authorized for operations"""
218
+ import os
219
+
220
+ import httpx
221
+
222
+ # Only check in GitHub Actions context
223
+ if os.getenv("GITHUB_ACTIONS") != "true":
224
+ return True
225
+
226
+ actor = os.getenv("GITHUB_ACTOR")
227
+ if not actor:
228
+ return True # No actor info, allow operation
229
+
230
+ try:
231
+ # Use existing GitHubInterface for consistency
232
+ github = get_github()
233
+
234
+ # Check collaborator permissions
235
+ perm_url = f"{github.base_url}/repos/{github.org}/{github.repo}/collaborators/{actor}/permission"
236
+
237
+ with httpx.Client() as client:
238
+ response = client.get(perm_url, headers=github.headers)
239
+ if response.status_code == 404:
240
+ return False # Not a collaborator
241
+ response.raise_for_status()
242
+
243
+ data = response.json()
244
+ permission = data.get("permission", "none")
245
+
246
+ # Allow write and admin permissions only
247
+ authorized = permission in ["write", "admin"]
248
+
249
+ if not authorized:
250
+ print(f"🚨 Unauthorized actor {actor} (permission: {permission})")
251
+ # Set blocked label for security
252
+ self.add_label("🎪 🔒 showtime-blocked")
253
+
254
+ return authorized
255
+
256
+ except Exception as e:
257
+ print(f"⚠️ Authorization check failed: {e}")
258
+ return True # Fail open for non-security operations
259
+
188
260
  def analyze(self, target_sha: str, pr_state: str = "open") -> AnalysisResult:
189
261
  """Analyze what actions are needed (read-only, for --check-only)
190
262
 
@@ -208,7 +280,7 @@ class PullRequest:
208
280
  build_needed = action_needed in ["create_environment", "rolling_update", "auto_sync"]
209
281
 
210
282
  # Determine if sync execution is needed
211
- sync_needed = action_needed != "no_action"
283
+ sync_needed = action_needed not in ["no_action", "blocked"]
212
284
 
213
285
  return AnalysisResult(
214
286
  action_needed=action_needed,
@@ -244,7 +316,15 @@ class PullRequest:
244
316
  # 1. Determine what action is needed
245
317
  action_needed = self._determine_action(target_sha)
246
318
 
247
- # 2. Atomic claim for environment changes (PR-level lock)
319
+ # 2. Check for blocked state (fast bailout)
320
+ if action_needed == "blocked":
321
+ return SyncResult(
322
+ success=False,
323
+ action_taken="blocked",
324
+ error="🔒 Showtime operations are blocked for this PR. Remove '🎪 🔒 showtime-blocked' label to re-enable.",
325
+ )
326
+
327
+ # 3. Atomic claim for environment changes (PR-level lock)
248
328
  if action_needed in ["create_environment", "rolling_update", "auto_sync"]:
249
329
  print(f"🔒 Claiming environment for {action_needed}...")
250
330
  if not self._atomic_claim(target_sha, action_needed, dry_run_github):
@@ -266,14 +346,14 @@ class PullRequest:
266
346
  # Phase 1: Docker build
267
347
  print("🐳 Building Docker image...")
268
348
  show.build_docker(dry_run_docker)
269
- show.status = "built"
270
349
  print("✅ Docker build completed")
271
- self._update_show_labels(show, dry_run_github)
272
350
 
273
351
  # Phase 2: AWS deployment
274
352
  print("☁️ Deploying to AWS ECS...")
353
+ self.set_show_status(show, "deploying")
275
354
  show.deploy_aws(dry_run_aws)
276
- show.status = "running"
355
+ self.set_show_status(show, "running")
356
+ self.set_active_show(show)
277
357
  print(f"✅ Deployment completed - environment running at {show.ip}:8080")
278
358
  self._update_show_labels(show, dry_run_github)
279
359
 
@@ -303,14 +383,14 @@ class PullRequest:
303
383
  # Phase 1: Docker build
304
384
  print("🐳 Building updated Docker image...")
305
385
  new_show.build_docker(dry_run_docker)
306
- new_show.status = "built"
307
386
  print("✅ Docker build completed")
308
- self._update_show_labels(new_show, dry_run_github)
309
387
 
310
388
  # Phase 2: Blue-green deployment
311
389
  print("☁️ Deploying updated environment...")
390
+ self.set_show_status(new_show, "deploying")
312
391
  new_show.deploy_aws(dry_run_aws)
313
- new_show.status = "running"
392
+ self.set_show_status(new_show, "running")
393
+ self.set_active_show(new_show)
314
394
  print(f"✅ Rolling update completed - new environment at {new_show.ip}:8080")
315
395
  self._update_show_labels(new_show, dry_run_github)
316
396
 
@@ -406,7 +486,7 @@ class PullRequest:
406
486
  if any(label == f"🎪 🎯 {show.sha}" for label in pr.labels):
407
487
  show_type = "active"
408
488
  # Check for building pointer
409
- elif any(label == f"🎪 🏗️ {show.sha}" for label in pr.labels):
489
+ elif show.status in ["building", "deploying"]:
410
490
  show_type = "building"
411
491
  # No pointer = orphaned
412
492
 
@@ -433,6 +513,14 @@ class PullRequest:
433
513
  # CRITICAL: Get fresh labels before any decisions
434
514
  self.refresh_labels()
435
515
 
516
+ # Check for blocked state first (fast bailout)
517
+ if "🎪 🔒 showtime-blocked" in self.labels:
518
+ return "blocked"
519
+
520
+ # Check authorization (security layer)
521
+ if not self._check_authorization():
522
+ return "blocked"
523
+
436
524
  target_sha_short = target_sha[:7] # Ensure we're working with short SHA
437
525
 
438
526
  # Get the specific show for the target SHA
@@ -515,7 +603,6 @@ class PullRequest:
515
603
  for label in new_labels:
516
604
  try:
517
605
  self.add_label(label)
518
- print(f" ✅ Added: {label}")
519
606
  except Exception as e:
520
607
  print(f" ❌ Failed to add {label}: {e}")
521
608
  raise
@@ -646,7 +733,6 @@ class PullRequest:
646
733
  and (
647
734
  label.startswith(f"🎪 {show.sha} ") # SHA-first format: 🎪 abc123f 📅 ...
648
735
  or label.startswith(f"🎪 🎯 {show.sha}") # Pointer format: 🎪 🎯 abc123f
649
- or label.startswith(f"🎪 🏗️ {show.sha}") # Building pointer: 🎪 🏗️ abc123f
650
736
  )
651
737
  }
652
738
  desired_labels = set(show.to_circus_labels())
@@ -704,9 +790,7 @@ class PullRequest:
704
790
  existing_labels = [
705
791
  label
706
792
  for label in self.labels
707
- if label.startswith(f"🎪 {show.sha} ")
708
- or label == f"🎪 🎯 {show.sha}"
709
- or label == f"🎪 🏗️ {show.sha}"
793
+ if label.startswith(f"🎪 {show.sha} ") or label == f"🎪 🎯 {show.sha}"
710
794
  ]
711
795
  print(f"🏷️ Removing existing labels for {show.sha}: {existing_labels}")
712
796
  for label in existing_labels:
showtime/core/show.py CHANGED
@@ -96,21 +96,24 @@ class Show:
96
96
 
97
97
  def to_circus_labels(self) -> List[str]:
98
98
  """Convert show state to circus tent emoji labels (per-SHA format)"""
99
+ from .emojis import CIRCUS_PREFIX, MEANING_TO_EMOJI
100
+
99
101
  if not self.created_at:
100
102
  self.created_at = datetime.utcnow().strftime("%Y-%m-%dT%H-%M")
101
103
 
102
104
  labels = [
103
- f"🎪 {self.sha} 🚦 {self.status}", # SHA-first status
104
- f"🎪 🎯 {self.sha}", # Active pointer (no value)
105
- f"🎪 {self.sha} 📅 {self.created_at}", # SHA-first timestamp
106
- f"🎪 {self.sha} ⌛ {self.ttl}", # SHA-first TTL
105
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['status']} {self.status}", # SHA-first status
106
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['created_at']} {self.created_at}", # SHA-first timestamp
107
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['ttl']} {self.ttl}", # SHA-first TTL
107
108
  ]
108
109
 
109
110
  if self.ip:
110
- labels.append(f"🎪 {self.sha} 🌐 {self.ip}:8080")
111
+ labels.append(f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['ip']} {self.ip}:8080")
111
112
 
112
113
  if self.requested_by:
113
- labels.append(f"🎪 {self.sha} 🤡 {self.requested_by}")
114
+ labels.append(
115
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['requested_by']} {self.requested_by}"
116
+ )
114
117
 
115
118
  return labels
116
119
 
@@ -158,7 +161,6 @@ class Show:
158
161
  def _build_docker_image(self) -> None:
159
162
  """Build Docker image for this environment"""
160
163
  import os
161
- import platform
162
164
  import subprocess
163
165
 
164
166
  tag = f"apache/superset:pr-{self.pr_number}-{self.sha}-ci"
@@ -187,19 +189,23 @@ class Show:
187
189
  # Add caching based on environment
188
190
  if is_ci:
189
191
  # Full registry caching in CI (Docker driver supports it)
190
- cmd.extend([
191
- "--cache-from",
192
- "type=registry,ref=apache/superset-cache:showtime",
193
- "--cache-to",
194
- "type=registry,mode=max,ref=apache/superset-cache:showtime",
195
- ])
192
+ cmd.extend(
193
+ [
194
+ "--cache-from",
195
+ "type=registry,ref=apache/superset-cache:showtime",
196
+ "--cache-to",
197
+ "type=registry,mode=max,ref=apache/superset-cache:showtime",
198
+ ]
199
+ )
196
200
  print("🐳 CI environment: Using full registry caching")
197
201
  else:
198
202
  # Local build: cache-from only (no cache export)
199
- cmd.extend([
200
- "--cache-from",
201
- "type=registry,ref=apache/superset-cache:showtime",
202
- ])
203
+ cmd.extend(
204
+ [
205
+ "--cache-from",
206
+ "type=registry,ref=apache/superset-cache:showtime",
207
+ ]
208
+ )
203
209
  print("🐳 Local environment: Using cache-from only (no export)")
204
210
 
205
211
  # Add --load only when explicitly requested for local testing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.5.12
3
+ Version: 0.5.18
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=Rdi2WLB7sEHeBvwFnKdebcbskh9xDjmvIwxcOs9VU_0,449
1
+ showtime/__init__.py,sha256=apR60gqUgO9WuV06_c-B1cXzwzdQRcEaY-uYSPrrX0A,449
2
2
  showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
3
  showtime/cli.py,sha256=8vIJT5TiqXuHDGxRBg6jV3oNv5nKrmDOs5OgltycPeI,31664
4
4
  showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
5
5
  showtime/core/aws.py,sha256=uTjJAvEBQMyTccS93WZeNPhfeKQhJgOQQ0BJdnQjvCU,35007
6
- showtime/core/emojis.py,sha256=MHEDuPIdfNiop4zbNLuviz3eY05QiftYSHHCVbkfKhw,2129
6
+ showtime/core/emojis.py,sha256=arK0N5Q5FLkvOkci-lacb3WS56LTvY8NjYRqt_lhP9s,2188
7
7
  showtime/core/git_validation.py,sha256=3dmSGpMDplDAmKWHUyoUEPgt3__8oTuBZxbfuhocT00,6831
8
8
  showtime/core/github.py,sha256=gMPJ5TOT6DdZk4y0XqW-C69I7O8A4eI40TgT4IFPqhQ,9623
9
9
  showtime/core/github_messages.py,sha256=MfgwCukrEsWWesMsuL8saciDgP4nS-gijzu8DXr-Alg,7450
10
- showtime/core/label_colors.py,sha256=efhbFnz_3nqEnEqmgyF6_hZbxtCu_fmb68BIIUpSsnk,3895
11
- showtime/core/pull_request.py,sha256=L9d0gHJihc6GYDWWM3oK-sGNu-yysUopMGXnqwP6I_4,28748
12
- showtime/core/show.py,sha256=FpxDm52LASCJvf8UF998AtNiVzfdYIwNEsPAsOAAwL0,9701
10
+ showtime/core/label_colors.py,sha256=gSe7EIMl4YjWkIgKHUvuaRSwgEB_B-NYQBxFFlF8Z3s,4065
11
+ showtime/core/pull_request.py,sha256=r-4tCjEjsZOcTk4cjw57yyhNzq1sLPt4SYa1S7RcDlQ,31998
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.12.dist-info/METADATA,sha256=orUqMEiS9J1OUBm2uH7o-7aHfIEKvp1CZ4e7z5gk70I,12053
15
- superset_showtime-0.5.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- superset_showtime-0.5.12.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
- superset_showtime-0.5.12.dist-info/RECORD,,
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,,