superset-showtime 0.1.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.

@@ -0,0 +1,285 @@
1
+ """
2
+ 🎪 Circus tent emoji label parsing and state management
3
+
4
+ Core logic for parsing GitHub labels with circus tent emoji patterns.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from typing import TYPE_CHECKING, List, Optional
10
+
11
+ if TYPE_CHECKING:
12
+ from .github import GitHubInterface
13
+
14
+
15
+ @dataclass
16
+ class Show:
17
+ """Single ephemeral environment state from circus labels"""
18
+
19
+ pr_number: int
20
+ sha: str # 7-char commit SHA
21
+ status: str # building, running, updating, failed
22
+ ip: Optional[str] = None # Environment IP address
23
+ created_at: Optional[str] = None # ISO timestamp
24
+ ttl: str = "24h" # 24h, 48h, close, etc.
25
+ requested_by: Optional[str] = None # GitHub username
26
+ config: str = "standard" # Configuration (alerts,debug)
27
+
28
+ @property
29
+ def aws_service_name(self) -> str:
30
+ """Deterministic ECS service name: pr-{pr_number}-{sha}"""
31
+ return f"pr-{self.pr_number}-{self.sha}"
32
+
33
+ @property
34
+ def aws_image_tag(self) -> str:
35
+ """Deterministic ECR image tag: pr-{pr_number}-{sha}-ci"""
36
+ return f"pr-{self.pr_number}-{self.sha}-ci"
37
+
38
+ @property
39
+ def is_active(self) -> bool:
40
+ """Check if this is the currently active show"""
41
+ return self.status == "running"
42
+
43
+ @property
44
+ def is_building(self) -> bool:
45
+ """Check if environment is currently building"""
46
+ return self.status == "building"
47
+
48
+ @property
49
+ def is_updating(self) -> bool:
50
+ """Check if environment is currently updating"""
51
+ return self.status == "updating"
52
+
53
+ def needs_update(self, latest_sha: str) -> bool:
54
+ """Check if environment needs update to latest SHA"""
55
+ return self.sha != latest_sha[:7]
56
+
57
+ def to_circus_labels(self) -> List[str]:
58
+ """Convert show state to circus tent emoji labels (per-SHA format)"""
59
+ if not self.created_at:
60
+ self.created_at = datetime.utcnow().strftime("%Y-%m-%dT%H-%M")
61
+
62
+ labels = [
63
+ f"🎪 {self.sha} 🚦 {self.status}", # SHA-first status
64
+ f"🎪 🎯 {self.sha}", # Active pointer (no value)
65
+ f"🎪 {self.sha} 📅 {self.created_at}", # SHA-first timestamp
66
+ f"🎪 {self.sha} ⌛ {self.ttl}", # SHA-first TTL
67
+ ]
68
+
69
+ if self.ip:
70
+ labels.append(f"🎪 {self.sha} 🌐 {self.ip}:8080")
71
+
72
+ if self.requested_by:
73
+ labels.append(f"🎪 {self.sha} 🤡 {self.requested_by}")
74
+
75
+ if self.config != "standard":
76
+ labels.append(f"🎪 {self.sha} ⚙️ {self.config}")
77
+
78
+ return labels
79
+
80
+ @classmethod
81
+ def from_circus_labels(cls, pr_number: int, labels: List[str], sha: str) -> Optional["Show"]:
82
+ """Create Show from circus tent labels for specific SHA"""
83
+ show_data = {
84
+ "pr_number": pr_number,
85
+ "sha": sha,
86
+ "status": "building", # default
87
+ }
88
+
89
+ for label in labels:
90
+ if not label.startswith("🎪 "):
91
+ continue
92
+
93
+ parts = label.split(" ", 3) # Split into 4 parts for per-SHA format
94
+
95
+ if len(parts) == 3: # Old format: 🎪 🎯 sha
96
+ emoji, value = parts[1], parts[2]
97
+ if emoji == "🎯" and value == sha: # Active pointer
98
+ pass # This SHA is active
99
+ elif len(parts) == 4: # SHA-first format: 🎪 sha 🚦 status
100
+ label_sha, emoji, value = parts[1], parts[2], parts[3]
101
+
102
+ if label_sha != sha: # Only process labels for this SHA
103
+ continue
104
+
105
+ if emoji == "🚦": # Status
106
+ show_data["status"] = value
107
+ elif emoji == "📅": # Timestamp
108
+ show_data["created_at"] = value
109
+ elif emoji == "🌐": # IP with port
110
+ show_data["ip"] = value.replace(":8080", "") # Remove port for storage
111
+ elif emoji == "⌛": # TTL
112
+ show_data["ttl"] = value
113
+ elif emoji == "🤡": # User (clown!)
114
+ show_data["requested_by"] = value
115
+ elif emoji == "⚙️": # Config
116
+ show_data["config"] = value
117
+
118
+ # Only return Show if we found relevant labels for this SHA
119
+ if any(label.endswith(f" {sha}") for label in labels if "🎯" in label or "🏗️" in label):
120
+ return cls(**show_data)
121
+
122
+ return None
123
+
124
+
125
+ class PullRequest:
126
+ """GitHub PR with its shows parsed from circus labels"""
127
+
128
+ def __init__(self, pr_number: int, labels: List[str]):
129
+ self.pr_number = pr_number
130
+ self.labels = labels
131
+ self._shows = self._parse_shows_from_labels()
132
+
133
+ @property
134
+ def shows(self) -> List[Show]:
135
+ """All shows found in labels"""
136
+ return self._shows
137
+
138
+ @property
139
+ def current_show(self) -> Optional[Show]:
140
+ """The currently active show (from 🎯 label)"""
141
+ # Find the SHA that's marked as active (🎯)
142
+ active_sha = None
143
+ for label in self.labels:
144
+ if label.startswith("🎪 🎯 "):
145
+ active_sha = label.split(" ")[2]
146
+ break
147
+
148
+ if not active_sha:
149
+ return None
150
+
151
+ # Find the show with that SHA
152
+ for show in self.shows:
153
+ if show.sha == active_sha:
154
+ return show
155
+
156
+ return None
157
+
158
+ @property
159
+ def building_show(self) -> Optional[Show]:
160
+ """Show currently being built (from 🏗️ label)"""
161
+ building_sha = None
162
+ for label in self.labels:
163
+ if label.startswith("🎪 🏗️ "):
164
+ building_sha = label.split(" ")[2]
165
+ break
166
+
167
+ if not building_sha:
168
+ return None
169
+
170
+ for show in self.shows:
171
+ if show.sha == building_sha:
172
+ return show
173
+
174
+ return None
175
+
176
+ @property
177
+ def circus_labels(self) -> List[str]:
178
+ """All circus tent labels"""
179
+ return [label for label in self.labels if label.startswith("🎪 ")]
180
+
181
+ def has_shows(self) -> bool:
182
+ """Check if PR has any shows"""
183
+ return len(self.shows) > 0
184
+
185
+ def get_show_by_sha(self, sha: str) -> Optional[Show]:
186
+ """Get specific show by SHA"""
187
+ for show in self.shows:
188
+ if show.sha == sha[:7]:
189
+ return show
190
+ return None
191
+
192
+ def _parse_shows_from_labels(self) -> List[Show]:
193
+ """Parse all shows from circus labels"""
194
+ shows = []
195
+
196
+ # Find all unique SHAs mentioned in labels
197
+ shas = set()
198
+ for label in self.labels:
199
+ if label.startswith("🎪 🎯 ") or label.startswith("🎪 🏗️ "):
200
+ sha = label.split(" ")[2]
201
+ shas.add(sha)
202
+
203
+ # Create Show object for each SHA
204
+ for sha in shas:
205
+ show = Show.from_circus_labels(self.pr_number, self.labels, sha)
206
+ if show:
207
+ shows.append(show)
208
+
209
+ return shows
210
+
211
+ @classmethod
212
+ def from_id(cls, pr_number: int, github: "GitHubInterface") -> "PullRequest":
213
+ """Load PR with current labels from GitHub"""
214
+ labels = github.get_labels(pr_number)
215
+ return cls(pr_number, labels)
216
+
217
+ def refresh_labels(self, github: "GitHubInterface") -> None:
218
+ """Refresh labels from GitHub and reparse shows"""
219
+ self.labels = github.get_labels(self.pr_number)
220
+ self._shows = self._parse_shows_from_labels()
221
+
222
+
223
+ # Utility functions for configuration label handling
224
+ def is_configuration_label(label: str) -> bool:
225
+ """Check if label is a configuration command"""
226
+ return label.startswith("🎪 conf-")
227
+
228
+
229
+ def parse_configuration_command(label: str) -> Optional[str]:
230
+ """
231
+ Parse configuration command from label
232
+
233
+ Args:
234
+ label: Label like "🎪 conf-enable-ALERTS"
235
+
236
+ Returns:
237
+ Command string like "enable-ALERTS" or None if not a config label
238
+ """
239
+ if not is_configuration_label(label):
240
+ return None
241
+
242
+ return label.replace("🎪 conf-", "")
243
+
244
+
245
+ def merge_config(current_config: str, command: str) -> str:
246
+ """
247
+ Merge new configuration command into existing config
248
+
249
+ Args:
250
+ current_config: Current config string like "standard,alerts"
251
+ command: New command like "enable-DASHBOARD_RBAC" or "disable-ALERTS"
252
+
253
+ Returns:
254
+ Updated config string
255
+ """
256
+ configs = current_config.split(",") if current_config != "standard" else []
257
+
258
+ # Handle feature flag commands
259
+ if command.startswith("enable-"):
260
+ feature = command.replace("enable-", "").lower()
261
+ configs = [c for c in configs if not c.startswith(f"no-{feature}")]
262
+ configs.append(feature)
263
+
264
+ elif command.startswith("disable-"):
265
+ feature = command.replace("disable-", "").lower()
266
+ configs = [c for c in configs if c != feature]
267
+ configs.append(f"no-{feature}")
268
+
269
+ # Handle debug toggle commands
270
+ elif command == "debug-on":
271
+ configs = [c for c in configs if c != "debug"]
272
+ configs.append("debug")
273
+
274
+ elif command == "debug-off":
275
+ configs = [c for c in configs if c != "debug"]
276
+
277
+ # Handle size commands
278
+ elif command.startswith("size-"):
279
+ size = command # Keep full size command
280
+ configs = [c for c in configs if not c.startswith("size-")]
281
+ configs.append(size)
282
+
283
+ # Return cleaned config
284
+ unique_configs = list(dict.fromkeys(configs)) # Remove duplicates, preserve order
285
+ return ",".join(unique_configs) if unique_configs else "standard"
@@ -0,0 +1,152 @@
1
+ """
2
+ 🎪 Showtime configuration management
3
+
4
+ Handles configuration for GitHub API, AWS credentials, and showtime settings.
5
+ """
6
+
7
+ import os
8
+ from dataclasses import asdict, dataclass
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import yaml
13
+ from rich.console import Console
14
+ from rich.prompt import Prompt
15
+
16
+ console = Console()
17
+
18
+
19
+ @dataclass
20
+ class ShowtimeConfig:
21
+ """Showtime configuration settings"""
22
+
23
+ # GitHub settings
24
+ github_token: str
25
+ github_org: str
26
+ github_repo: str
27
+
28
+ # AWS settings
29
+ aws_profile: str = "default"
30
+ aws_region: str = "us-west-2"
31
+
32
+ # Circus settings
33
+ default_ttl: str = "24h"
34
+ default_size: str = "standard"
35
+
36
+ # ECS settings
37
+ ecs_cluster: str = "superset-ci"
38
+ ecs_task_family: str = "superset-ci"
39
+ ecr_repository: str = "superset-ci"
40
+
41
+ @property
42
+ def config_path(self) -> Path:
43
+ """Path to configuration file"""
44
+ config_dir = Path.home() / ".showtime"
45
+ config_dir.mkdir(exist_ok=True)
46
+ return config_dir / "config.yml"
47
+
48
+ @classmethod
49
+ def get_config_path(cls) -> Path:
50
+ """Get path to configuration file"""
51
+ config_dir = Path.home() / ".showtime"
52
+ config_dir.mkdir(exist_ok=True)
53
+ return config_dir / "config.yml"
54
+
55
+ @classmethod
56
+ def load(cls) -> "ShowtimeConfig":
57
+ """Load configuration from file"""
58
+ config_path = cls.get_config_path()
59
+
60
+ if not config_path.exists():
61
+ raise FileNotFoundError(
62
+ f"Configuration file not found at {config_path}. "
63
+ f"Run 'showtime init' to create it."
64
+ )
65
+
66
+ with open(config_path) as f:
67
+ data = yaml.safe_load(f)
68
+
69
+ return cls(**data)
70
+
71
+ def save(self) -> None:
72
+ """Save configuration to file"""
73
+ with open(self.config_path, "w") as f:
74
+ yaml.safe_dump(asdict(self), f, default_flow_style=False)
75
+
76
+ @classmethod
77
+ def create_interactive(
78
+ cls,
79
+ github_token: Optional[str] = None,
80
+ aws_profile: Optional[str] = None,
81
+ region: Optional[str] = None,
82
+ ) -> "ShowtimeConfig":
83
+ """Create configuration interactively"""
84
+
85
+ console.print("🎪 [bold blue]Welcome to Showtime! Let's set up your circus...[/bold blue]")
86
+ console.print()
87
+
88
+ # GitHub configuration
89
+ if not github_token:
90
+ github_token = Prompt.ask(
91
+ "GitHub personal access token", default=os.getenv("GITHUB_TOKEN", ""), password=True
92
+ )
93
+
94
+ github_org = Prompt.ask("GitHub organization", default="apache")
95
+
96
+ github_repo = Prompt.ask("GitHub repository", default="superset")
97
+
98
+ # AWS configuration
99
+ if not aws_profile:
100
+ aws_profile = Prompt.ask("AWS profile", default="default")
101
+
102
+ if not region:
103
+ region = Prompt.ask("AWS region", default="us-west-2")
104
+
105
+ # Circus settings
106
+ default_ttl = Prompt.ask(
107
+ "Default environment TTL",
108
+ default="24h",
109
+ choices=["24h", "48h", "1w", "close", "manual"],
110
+ )
111
+
112
+ default_size = Prompt.ask(
113
+ "Default environment size", default="standard", choices=["standard", "large"]
114
+ )
115
+
116
+ # ECS settings (with smart defaults)
117
+ ecs_cluster = Prompt.ask("ECS cluster name", default="superset-ci")
118
+
119
+ console.print()
120
+ console.print("🎪 [bold green]Configuration complete![/bold green]")
121
+
122
+ return cls(
123
+ github_token=github_token,
124
+ github_org=github_org,
125
+ github_repo=github_repo,
126
+ aws_profile=aws_profile,
127
+ aws_region=region,
128
+ default_ttl=default_ttl,
129
+ default_size=default_size,
130
+ ecs_cluster=ecs_cluster,
131
+ )
132
+
133
+ def validate(self) -> bool:
134
+ """Validate configuration settings"""
135
+ errors = []
136
+
137
+ if not self.github_token:
138
+ errors.append("GitHub token is required")
139
+
140
+ if not self.github_org:
141
+ errors.append("GitHub organization is required")
142
+
143
+ if not self.github_repo:
144
+ errors.append("GitHub repository is required")
145
+
146
+ if errors:
147
+ console.print("🎪 [bold red]Configuration errors:[/bold red]")
148
+ for error in errors:
149
+ console.print(f" • {error}")
150
+ return False
151
+
152
+ return True
@@ -0,0 +1,86 @@
1
+ """
2
+ 🎪 Emoji mappings and constants for circus tent state management
3
+
4
+ Central place for all emoji meanings and mappings.
5
+ """
6
+
7
+ # Core circus tent emoji
8
+ CIRCUS_PREFIX = "🎪"
9
+
10
+ # Meta emoji dictionary
11
+ EMOJI_MEANINGS = {
12
+ # Status indicators
13
+ "🚦": "status", # Traffic light for environment status
14
+ "🏗️": "building", # Construction for building environments
15
+ "🎯": "active", # Target for currently active environment
16
+ # Metadata
17
+ "📅": "created_at", # Calendar for creation timestamp
18
+ "🌐": "ip", # Globe for IP address
19
+ "⌛": "ttl", # Hourglass for time-to-live
20
+ "🤡": "requested_by", # Clown for who requested (circus theme!)
21
+ "⚙️": "config", # Gear for configuration
22
+ }
23
+
24
+ # Reverse mapping for creating labels
25
+ MEANING_TO_EMOJI = {v: k for k, v in EMOJI_MEANINGS.items()}
26
+
27
+ # Status display emojis (for CLI output)
28
+ STATUS_DISPLAY = {
29
+ "building": "🏗️",
30
+ "running": "🟢",
31
+ "updating": "🔄",
32
+ "failed": "❌",
33
+ "configuring": "⚙️",
34
+ "stopping": "🛑",
35
+ }
36
+
37
+ # Configuration command prefixes
38
+ CONFIG_PREFIX = f"{CIRCUS_PREFIX} conf-"
39
+
40
+
41
+ def create_circus_label(emoji_key: str, value: str) -> str:
42
+ """Create a circus tent label with proper spacing"""
43
+ emoji = MEANING_TO_EMOJI.get(emoji_key)
44
+ if not emoji:
45
+ raise ValueError(f"Unknown emoji key: {emoji_key}")
46
+
47
+ return f"{CIRCUS_PREFIX} {emoji} {value}"
48
+
49
+
50
+ def parse_circus_label(label: str) -> tuple[str, str]:
51
+ """
52
+ Parse a circus tent label into emoji meaning and value
53
+
54
+ Args:
55
+ label: Label like "🎪 🚦 running"
56
+
57
+ Returns:
58
+ Tuple of (meaning, value) like ("status", "running")
59
+
60
+ Raises:
61
+ ValueError: If not a valid circus label
62
+ """
63
+ if not label.startswith(f"{CIRCUS_PREFIX} "):
64
+ raise ValueError(f"Not a circus label: {label}")
65
+
66
+ parts = label.split(" ", 2)
67
+ if len(parts) < 3:
68
+ raise ValueError(f"Invalid circus label format: {label}")
69
+
70
+ emoji, value = parts[1], parts[2]
71
+ meaning = EMOJI_MEANINGS.get(emoji)
72
+
73
+ if not meaning:
74
+ raise ValueError(f"Unknown circus emoji: {emoji}")
75
+
76
+ return meaning, value
77
+
78
+
79
+ def is_circus_label(label: str) -> bool:
80
+ """Check if label is a circus tent label"""
81
+ return label.startswith(f"{CIRCUS_PREFIX} ")
82
+
83
+
84
+ def is_configuration_command(label: str) -> bool:
85
+ """Check if label is a configuration command"""
86
+ return label.startswith(CONFIG_PREFIX)