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.
- showtime/__init__.py +21 -0
- showtime/__main__.py +8 -0
- showtime/cli.py +1361 -0
- showtime/commands/__init__.py +1 -0
- showtime/commands/start.py +40 -0
- showtime/core/__init__.py +1 -0
- showtime/core/aws.py +758 -0
- showtime/core/circus.py +285 -0
- showtime/core/config.py +152 -0
- showtime/core/emojis.py +86 -0
- showtime/core/github.py +214 -0
- showtime/data/ecs-task-definition.json +59 -0
- superset_showtime-0.1.0.dist-info/METADATA +391 -0
- superset_showtime-0.1.0.dist-info/RECORD +16 -0
- superset_showtime-0.1.0.dist-info/WHEEL +4 -0
- superset_showtime-0.1.0.dist-info/entry_points.txt +3 -0
showtime/core/circus.py
ADDED
|
@@ -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"
|
showtime/core/config.py
ADDED
|
@@ -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
|
showtime/core/emojis.py
ADDED
|
@@ -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)
|