redis-flags 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.
File without changes
@@ -0,0 +1 @@
1
+ from . import flag, user, cohort, history
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from ..config import get_env, get_redis_url
8
+ from ..connection import get_client
9
+ from ..output import (
10
+ print_cohorts_table, print_cohort_panel,
11
+ print_success, print_error
12
+ )
13
+ from redis_feature_flags import FeatureFlags
14
+ from redis_feature_flags.exceptions import FlagNotFoundError
15
+
16
+ app = typer.Typer()
17
+
18
+
19
+ def get_flags(env: Optional[str], redis_url: Optional[str]) -> FeatureFlags:
20
+ resolved_env = get_env(env)
21
+ resolved_url = get_redis_url(redis_url)
22
+ client = get_client(resolved_url)
23
+ return FeatureFlags(client, env=resolved_env)
24
+
25
+
26
+ @app.command("create-cohort")
27
+ def create_cohort(
28
+ cohort_name: str = typer.Argument(..., help="Cohort name"),
29
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
30
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
31
+ ):
32
+ """
33
+ Create a new cohort.
34
+
35
+ Example:
36
+ redis-flags create-cohort beta-testers
37
+ """
38
+ flags = get_flags(env, redis_url)
39
+ flags.create_cohort(cohort_name)
40
+ print_success(f"Created cohort [bold]{cohort_name}[/bold]")
41
+
42
+
43
+ @app.command("delete-cohort")
44
+ def delete_cohort(
45
+ cohort_name: str = typer.Argument(..., help="Cohort name"),
46
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
47
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
48
+ confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
49
+ ):
50
+ """
51
+ Delete a cohort and clean up all member reverse index entries.
52
+
53
+ Example:
54
+ redis-flags delete-cohort beta-testers
55
+ redis-flags delete-cohort beta-testers --yes
56
+ """
57
+ if not confirm:
58
+ typer.confirm(
59
+ f"Delete cohort '{cohort_name}'? This cannot be undone.",
60
+ abort=True,
61
+ )
62
+ flags = get_flags(env, redis_url)
63
+ flags._cohorts.delete(cohort_name)
64
+ print_success(f"Deleted cohort [bold]{cohort_name}[/bold]")
65
+
66
+
67
+ @app.command("add-to-cohort")
68
+ def add_to_cohort(
69
+ cohort_name: str = typer.Argument(..., help="Cohort name"),
70
+ user_id: str = typer.Argument(..., help="User ID to add"),
71
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
72
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
73
+ ):
74
+ """
75
+ Add a user to a cohort.
76
+
77
+ Example:
78
+ redis-flags add-to-cohort beta-testers alice
79
+ """
80
+ flags = get_flags(env, redis_url)
81
+ flags.add_to_cohort(cohort_name, user_id)
82
+ print_success(f"Added [bold]{user_id}[/bold] to cohort [bold]{cohort_name}[/bold]")
83
+
84
+
85
+ @app.command("remove-from-cohort")
86
+ def remove_from_cohort(
87
+ cohort_name: str = typer.Argument(..., help="Cohort name"),
88
+ user_id: str = typer.Argument(..., help="User ID to remove"),
89
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
90
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
91
+ ):
92
+ """
93
+ Remove a user from a cohort.
94
+
95
+ Example:
96
+ redis-flags remove-from-cohort beta-testers alice
97
+ """
98
+ flags = get_flags(env, redis_url)
99
+ flags.remove_from_cohort(cohort_name, user_id)
100
+ print_success(f"Removed [bold]{user_id}[/bold] from cohort [bold]{cohort_name}[/bold]")
101
+
102
+
103
+ @app.command("list-cohorts")
104
+ def list_cohorts(
105
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
106
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
107
+ ):
108
+ """
109
+ List all cohorts.
110
+
111
+ Example:
112
+ redis-flags list-cohorts
113
+ """
114
+ flags = get_flags(env, redis_url)
115
+ cohorts = flags._cohorts.list_cohorts()
116
+ print_cohorts_table(cohorts)
117
+
118
+
119
+ @app.command("inspect-cohort")
120
+ def inspect_cohort(
121
+ cohort_name: str = typer.Argument(..., help="Cohort name"),
122
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
123
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
124
+ ):
125
+ """
126
+ Show all members of a cohort.
127
+
128
+ Example:
129
+ redis-flags inspect-cohort beta-testers
130
+ """
131
+ flags = get_flags(env, redis_url)
132
+ members = flags._cohorts.get_members(cohort_name)
133
+ print_cohort_panel(cohort_name, list(members))
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from ..config import get_env, get_redis_url
9
+ from ..connection import get_client
10
+ from ..output import (
11
+ print_flags_table, print_flag_panel,
12
+ print_success, print_error
13
+ )
14
+ from redis_feature_flags import FeatureFlags
15
+ from redis_feature_flags.exceptions import (
16
+ FlagNotFoundError, InvalidRolloutError
17
+ )
18
+
19
+ import getpass
20
+
21
+ app = typer.Typer()
22
+ console = Console()
23
+
24
+
25
+ def get_flags(env: Optional[str], redis_url: Optional[str]) -> FeatureFlags:
26
+ """Build FeatureFlags instance from CLI options."""
27
+ resolved_env = get_env(env)
28
+ resolved_url = get_redis_url(redis_url)
29
+ client = get_client(resolved_url)
30
+ return FeatureFlags(client, env=resolved_env)
31
+
32
+
33
+ @app.command("create")
34
+ def create(
35
+ flag_name: str = typer.Argument(..., help="Flag name"),
36
+ rollout: int = typer.Option(0, "--rollout", "-r", help="Rollout percentage 0-100"),
37
+ created_by: str = typer.Option(getpass.getuser(), "--created-by", help="Who is creating this flag"),
38
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
39
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
40
+ ):
41
+ """
42
+ Create a new feature flag.
43
+
44
+ Example:
45
+ redis-flags create dark_mode
46
+ redis-flags create dark_mode --rollout 10
47
+ redis-flags create dark_mode --rollout 10 --created-by alice
48
+ """
49
+ try:
50
+ flags = get_flags(env, redis_url)
51
+ flags.create(flag_name, rollout=rollout, created_by=created_by)
52
+ print_success(f"Created flag [bold]{flag_name}[/bold] (rollout: {rollout}%)")
53
+ except InvalidRolloutError as e:
54
+ print_error(str(e))
55
+ raise typer.Exit(1)
56
+
57
+
58
+ @app.command("enable")
59
+ def enable(
60
+ flag_name: str = typer.Argument(..., help="Flag name"),
61
+ updated_by: str = typer.Option(getpass.getuser(), "--updated-by", help="Who is enabling this flag"),
62
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
63
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
64
+ ):
65
+ """
66
+ Enable a feature flag.
67
+
68
+ Example:
69
+ redis-flags enable dark_mode
70
+ redis-flags enable dark_mode --updated-by alice
71
+ """
72
+ try:
73
+ flags = get_flags(env, redis_url)
74
+ flags.enable(flag_name, updated_by=updated_by)
75
+ print_success(f"Enabled [bold]{flag_name}[/bold]")
76
+ except FlagNotFoundError as e:
77
+ print_error(str(e))
78
+ raise typer.Exit(1)
79
+
80
+
81
+ @app.command("disable")
82
+ def disable(
83
+ flag_name: str = typer.Argument(..., help="Flag name"),
84
+ updated_by: str = typer.Option(getpass.getuser(), "--updated-by", help="Who is disabling this flag"),
85
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
86
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
87
+ ):
88
+ """
89
+ Disable a feature flag — instant kill switch.
90
+
91
+ Example:
92
+ redis-flags disable dark_mode
93
+ redis-flags disable dark_mode --updated-by alice
94
+ """
95
+ try:
96
+ flags = get_flags(env, redis_url)
97
+ flags.disable(flag_name, updated_by=updated_by)
98
+ print_success(f"Disabled [bold]{flag_name}[/bold]")
99
+ except FlagNotFoundError as e:
100
+ print_error(str(e))
101
+ raise typer.Exit(1)
102
+
103
+
104
+ @app.command("set-rollout")
105
+ def set_rollout(
106
+ flag_name: str = typer.Argument(..., help="Flag name"),
107
+ percent: int = typer.Argument(..., help="Rollout percentage 0-100"),
108
+ updated_by: str = typer.Option(getpass.getuser(), "--updated-by", help="Who is updating this flag"),
109
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
110
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
111
+ ):
112
+ """
113
+ Set the rollout percentage for a flag.
114
+
115
+ Example:
116
+ redis-flags set-rollout dark_mode 50
117
+ redis-flags set-rollout dark_mode 100
118
+ """
119
+ try:
120
+ flags = get_flags(env, redis_url)
121
+ flags.set_rollout(flag_name, percent, updated_by=updated_by)
122
+ print_success(f"Rollout for [bold]{flag_name}[/bold] set to {percent}%")
123
+ except (FlagNotFoundError, InvalidRolloutError) as e:
124
+ print_error(str(e))
125
+ raise typer.Exit(1)
126
+
127
+
128
+ @app.command("delete")
129
+ def delete(
130
+ flag_name: str = typer.Argument(..., help="Flag name"),
131
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
132
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
133
+ confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
134
+ ):
135
+ """
136
+ Delete a feature flag permanently.
137
+
138
+ Example:
139
+ redis-flags delete dark_mode
140
+ redis-flags delete dark_mode --yes
141
+ """
142
+ if not confirm:
143
+ typer.confirm(
144
+ f"Delete flag '{flag_name}'? This cannot be undone.",
145
+ abort=True,
146
+ )
147
+ try:
148
+ flags = get_flags(env, redis_url)
149
+ flags.delete(flag_name)
150
+ print_success(f"Deleted flag [bold]{flag_name}[/bold]")
151
+ except FlagNotFoundError as e:
152
+ print_error(str(e))
153
+ raise typer.Exit(1)
154
+
155
+
156
+ @app.command("list")
157
+ def list_flags(
158
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
159
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
160
+ ):
161
+ """
162
+ List all feature flags.
163
+
164
+ Example:
165
+ redis-flags list
166
+ redis-flags --env prod list
167
+ """
168
+ flags = get_flags(env, redis_url)
169
+ flag_names = flags.list_flags()
170
+ flag_data = []
171
+ for name in flag_names:
172
+ data = flags.get(name)
173
+ data["name"] = name
174
+ flag_data.append(data)
175
+ print_flags_table(flag_data)
176
+
177
+
178
+ @app.command("inspect")
179
+ def inspect(
180
+ flag_name: str = typer.Argument(..., help="Flag name"),
181
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
182
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
183
+ ):
184
+ """
185
+ Show detailed information about a flag including users and cohorts.
186
+
187
+ Example:
188
+ redis-flags inspect dark_mode
189
+ """
190
+ try:
191
+ flags = get_flags(env, redis_url)
192
+ resolved_env = get_env(env)
193
+ resolved_url = get_redis_url(redis_url)
194
+ client = get_client(resolved_url)
195
+
196
+ from redis_feature_flags.schema import SchemaKeys
197
+ schema = SchemaKeys(env=resolved_env)
198
+
199
+ data = flags.get(flag_name)
200
+ data["name"] = flag_name
201
+
202
+ users = [
203
+ u.decode() for u in
204
+ client.smembers(schema.flag_users(flag_name))
205
+ ]
206
+ cohorts = [
207
+ c.decode() for c in
208
+ client.smembers(schema.flag_cohorts(flag_name))
209
+ ]
210
+ print_flag_panel(data, users, cohorts)
211
+ except FlagNotFoundError as e:
212
+ print_error(str(e))
213
+ raise typer.Exit(1)
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ app = typer.Typer()
9
+ console = Console()
10
+
11
+
12
+ @app.command("history")
13
+ def history(
14
+ flag_name: str = typer.Argument(..., help="Flag name"),
15
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
16
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
17
+ ):
18
+ """
19
+ Show version history for a flag.
20
+
21
+ Example:
22
+ redis-flags history dark_mode
23
+ """
24
+ console.print("[yellow]History coming in v1.1[/yellow]")
25
+
26
+
27
+ @app.command("rollback")
28
+ def rollback(
29
+ flag_name: str = typer.Argument(..., help="Flag name"),
30
+ version: int = typer.Option(..., "--version", "-v", help="Version to roll back to"),
31
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
32
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
33
+ ):
34
+ """
35
+ Roll back a flag to a previous version.
36
+
37
+ Example:
38
+ redis-flags rollback dark_mode --version 2
39
+ """
40
+ console.print("[yellow]Rollback coming in v1.1[/yellow]")
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from ..config import get_env, get_redis_url
8
+ from ..connection import get_client
9
+ from ..output import print_success, print_error
10
+ from redis_feature_flags import FeatureFlags
11
+ from redis_feature_flags.exceptions import FlagNotFoundError
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ def get_flags(env: Optional[str], redis_url: Optional[str]) -> FeatureFlags:
17
+ resolved_env = get_env(env)
18
+ resolved_url = get_redis_url(redis_url)
19
+ client = get_client(resolved_url)
20
+ return FeatureFlags(client, env=resolved_env)
21
+
22
+
23
+ @app.command("add-user")
24
+ def add_user(
25
+ flag_name: str = typer.Argument(..., help="Flag name"),
26
+ user_id: str = typer.Argument(..., help="User ID to add"),
27
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
28
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
29
+ ):
30
+ """
31
+ Add a user to a flag's allowlist.
32
+
33
+ Example:
34
+ redis-flags add-user dark_mode alice
35
+ """
36
+ try:
37
+ flags = get_flags(env, redis_url)
38
+ flags.add_user(flag_name, user_id)
39
+ print_success(f"Added [bold]{user_id}[/bold] to flag [bold]{flag_name}[/bold]")
40
+ except FlagNotFoundError as e:
41
+ print_error(str(e))
42
+ raise typer.Exit(1)
43
+
44
+
45
+ @app.command("remove-user")
46
+ def remove_user(
47
+ flag_name: str = typer.Argument(..., help="Flag name"),
48
+ user_id: str = typer.Argument(..., help="User ID to remove"),
49
+ env: Optional[str] = typer.Option(None, "--env", help="Environment override"),
50
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL override"),
51
+ ):
52
+ """
53
+ Remove a user from a flag's allowlist.
54
+
55
+ Example:
56
+ redis-flags remove-user dark_mode alice
57
+ """
58
+ flags = get_flags(env, redis_url)
59
+ flags.remove_user(flag_name, user_id)
60
+ print_success(f"Removed [bold]{user_id}[/bold] from flag [bold]{flag_name}[/bold]")
redis_flags/config.py ADDED
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ if sys.version_info >= (3, 11):
8
+ import tomllib
9
+ else:
10
+ import tomli as tomllib
11
+
12
+ import tomli_w
13
+
14
+ CONFIG_PATH = Path.home() / ".redis-flags.toml"
15
+
16
+
17
+ def read_config() -> dict:
18
+ """
19
+ Read config from ~/.redis-flags.toml.
20
+ Returns empty dict if file does not exist.
21
+ """
22
+ if not CONFIG_PATH.exists():
23
+ return {}
24
+ with open(CONFIG_PATH, "rb") as f:
25
+ return tomllib.load(f)
26
+
27
+
28
+ def write_config(data: dict) -> None:
29
+ """
30
+ Write config to ~/.redis-flags.toml.
31
+ Creates the file if it does not exist.
32
+ """
33
+ with open(CONFIG_PATH, "wb") as f:
34
+ tomli_w.dump(data, f)
35
+
36
+
37
+ def get_env(env_override: Optional[str] = None) -> str:
38
+ """
39
+ Resolve the active environment.
40
+
41
+ Priority:
42
+ 1. --env flag passed directly to command
43
+ 2. env saved in ~/.redis-flags.toml
44
+ 3. Neither set → raise error with helpful message
45
+
46
+ Args:
47
+ env_override: value from --env flag. None if not passed.
48
+
49
+ Returns:
50
+ The active environment string e.g. "prod", "staging", "dev"
51
+
52
+ Raises:
53
+ SystemExit: if no environment is set anywhere.
54
+ """
55
+ if env_override:
56
+ return env_override
57
+
58
+ config = read_config()
59
+ env = config.get("env")
60
+
61
+ if not env:
62
+ from rich.console import Console
63
+ console = Console()
64
+ console.print("\n[red]Error:[/red] No environment set.\n")
65
+ console.print(" Set a default environment:")
66
+ console.print(" [cyan]redis-flags use prod[/cyan]")
67
+ console.print(" [cyan]redis-flags use staging[/cyan]")
68
+ console.print(" [cyan]redis-flags use dev[/cyan]\n")
69
+ console.print(" Or specify for this command:")
70
+ console.print(" [cyan]redis-flags --env prod list[/cyan]\n")
71
+ raise SystemExit(1)
72
+
73
+ return env
74
+
75
+
76
+ def get_redis_url(url_override: Optional[str] = None) -> str:
77
+ """
78
+ Resolve the Redis URL.
79
+
80
+ Priority:
81
+ 1. --redis-url flag passed directly to command
82
+ 2. redis_url saved in ~/.redis-flags.toml
83
+ 3. Default: redis://localhost:6379
84
+
85
+ Args:
86
+ url_override: value from --redis-url flag. None if not passed.
87
+
88
+ Returns:
89
+ Redis URL string.
90
+ """
91
+ if url_override:
92
+ return url_override
93
+
94
+ config = read_config()
95
+ return config.get("redis_url", "redis://localhost:6379")
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import redis
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+
9
+ def get_client(redis_url: str) -> redis.Redis:
10
+ """
11
+ Create and verify a Redis client connection.
12
+
13
+ Args:
14
+ redis_url: Full Redis URL e.g. redis://localhost:6379
15
+ Supports auth: redis://:password@host:6379
16
+
17
+ Returns:
18
+ Connected Redis client.
19
+
20
+ Raises:
21
+ SystemExit: if Redis is unreachable.
22
+ """
23
+ try:
24
+ client = redis.Redis.from_url(redis_url, decode_responses=False)
25
+ client.ping()
26
+ return client
27
+ except redis.ConnectionError:
28
+ console.print(f"\n[red]Error:[/red] Cannot connect to Redis at {redis_url}\n")
29
+ console.print(" Start Redis locally:")
30
+ console.print(" [cyan]brew services start redis[/cyan] (macOS)")
31
+ console.print(" [cyan]sudo systemctl start redis[/cyan] (Linux)")
32
+ console.print(" [cyan]docker run -p 6379:6379 redis[/cyan] (Docker)\n")
33
+ console.print(" Or set a custom Redis URL:")
34
+ console.print(" [cyan]redis-flags --redis-url redis://your-host:6379 list[/cyan]\n")
35
+ raise SystemExit(1)
36
+ except redis.AuthenticationError:
37
+ console.print(f"\n[red]Error:[/red] Redis authentication failed.\n")
38
+ console.print(" Provide credentials in the URL:")
39
+ console.print(" [cyan]redis-flags --redis-url redis://:password@host:6379 list[/cyan]\n")
40
+ raise SystemExit(1)
redis_flags/main.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ from .config import read_config, write_config, get_env, get_redis_url
10
+ from .connection import get_client
11
+ from .commands import flag, user, cohort, history
12
+
13
+ app = typer.Typer(
14
+ name="redis-flags",
15
+ help="Manage Redis feature flags from your terminal.",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+ app.add_typer(flag.app, name="", help="")
20
+ app.add_typer(user.app, name="", help="")
21
+ app.add_typer(cohort.app, name="", help="")
22
+ app.add_typer(history.app, name="", help="")
23
+
24
+ console = Console()
25
+
26
+
27
+ @app.command("use")
28
+ def use_env(
29
+ env: str = typer.Argument(..., help="Environment to use e.g. prod staging dev"),
30
+ ):
31
+ """
32
+ Set the active environment for all subsequent commands.
33
+
34
+ Example:
35
+ redis-flags use prod
36
+ redis-flags use staging
37
+ redis-flags use dev
38
+ """
39
+ config = read_config()
40
+ config["env"] = env
41
+ write_config(config)
42
+ console.print(f"[green]✓[/green] Environment set to [bold]{env}[/bold]")
43
+
44
+
45
+ @app.command("status")
46
+ def status(
47
+ redis_url: Optional[str] = typer.Option(None, "--redis-url", help="Redis URL"),
48
+ ):
49
+ """
50
+ Show current context — environment and Redis connection.
51
+
52
+ Example:
53
+ redis-flags status
54
+ """
55
+ config = read_config()
56
+ env = config.get("env", "[red]not set[/red]")
57
+ url = get_redis_url(redis_url)
58
+
59
+ connected = False
60
+ try:
61
+ client = get_client(url)
62
+ connected = True
63
+ except SystemExit:
64
+ pass
65
+
66
+ connection_status = "[green]connected ✓[/green]" if connected else "[red]unreachable ✗[/red]"
67
+
68
+ lines = [
69
+ f"[bold]Environment[/bold] {env}",
70
+ f"[bold]Redis URL[/bold] {url}",
71
+ f"[bold]Redis[/bold] {connection_status}",
72
+ ]
73
+
74
+ console.print(Panel(
75
+ "\n".join(lines),
76
+ title="[bold cyan]Current context[/bold cyan]",
77
+ expand=False,
78
+ ))
redis_flags/output.py ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+ from rich import box
10
+ from datetime import datetime, timezone
11
+
12
+ console = Console()
13
+
14
+
15
+ def format_enabled(value: str) -> str:
16
+ """Format enabled field as colored yes/no."""
17
+ if value == "1":
18
+ return "[green]yes[/green]"
19
+ return "[red]no[/red]"
20
+
21
+
22
+ def format_rollout(value: str) -> str:
23
+ """Format rollout as percentage string."""
24
+ return f"{value}%"
25
+
26
+ def format_timestamp(value: str) -> str:
27
+ if not value or value == "0":
28
+ return "never"
29
+ try:
30
+ dt = datetime.fromtimestamp(int(value), tz=timezone.utc)
31
+ return dt.strftime("%Y-%m-%d %H:%M UTC")
32
+ except (ValueError, OSError):
33
+ return value
34
+
35
+
36
+ def print_flags_table(flags: List[Dict[str, Any]]) -> None:
37
+ """
38
+ Print a list of flags as a rich table.
39
+ Used by: redis-flags list
40
+ """
41
+ if not flags:
42
+ console.print("[yellow]No flags found.[/yellow]")
43
+ return
44
+
45
+ table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan")
46
+ table.add_column("Flag", style="bold")
47
+ table.add_column("Enabled")
48
+ table.add_column("Rollout")
49
+ table.add_column("Updated by")
50
+ table.add_column("Updated at")
51
+
52
+ for flag in flags:
53
+ table.add_row(
54
+ flag.get("name", ""),
55
+ format_enabled(flag.get("enabled", "0")),
56
+ format_rollout(flag.get("rollout", "0")),
57
+ flag.get("updated_by", ""),
58
+ format_timestamp(flag.get("updated_at", "0")),
59
+ )
60
+
61
+ console.print(table)
62
+
63
+
64
+ def print_flag_panel(
65
+ flag: Dict[str, Any],
66
+ users: List[str],
67
+ cohorts: List[str],
68
+ ) -> None:
69
+ """
70
+ Print detailed flag info as a rich panel.
71
+ Used by: redis-flags inspect {flag_name}
72
+ """
73
+ lines = []
74
+ lines.append(f"[bold]Enabled[/bold] {format_enabled(flag.get('enabled', '0'))}")
75
+ lines.append(f"[bold]Rollout[/bold] {format_rollout(flag.get('rollout', '0'))}")
76
+ lines.append(f"[bold]Version[/bold] {flag.get('flag_version', '1')}")
77
+ lines.append(f"[bold]Expires[/bold] {format_timestamp(flag.get('expires_at', '0'))}")
78
+ lines.append(f"[bold]Created by[/bold] {flag.get('created_by', '')}")
79
+ lines.append(f"[bold]Created at[/bold] {format_timestamp(flag.get('created_at', '0'))}")
80
+ lines.append(f"[bold]Updated by[/bold] {flag.get('updated_by', '')}")
81
+ lines.append(f"[bold]Updated at[/bold] {format_timestamp(flag.get('updated_at', '0'))}")
82
+
83
+ if users:
84
+ lines.append("")
85
+ lines.append("[bold]Users[/bold]")
86
+ for user in sorted(users):
87
+ lines.append(f" {user}")
88
+ else:
89
+ lines.append("")
90
+ lines.append("[bold]Users[/bold] [dim]none[/dim]")
91
+
92
+ if cohorts:
93
+ lines.append("")
94
+ lines.append("[bold]Cohorts[/bold]")
95
+ for cohort in sorted(cohorts):
96
+ lines.append(f" {cohort}")
97
+ else:
98
+ lines.append("")
99
+ lines.append("[bold]Cohorts[/bold] [dim]none[/dim]")
100
+
101
+ console.print(Panel(
102
+ "\n".join(lines),
103
+ title=f"[bold cyan]{flag.get('name', '')}[/bold cyan]",
104
+ expand=False,
105
+ ))
106
+
107
+
108
+ def print_cohorts_table(cohorts: List[str]) -> None:
109
+ """
110
+ Print a list of cohorts as a rich table.
111
+ Used by: redis-flags list-cohorts
112
+ """
113
+ if not cohorts:
114
+ console.print("[yellow]No cohorts found.[/yellow]")
115
+ return
116
+
117
+ table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan")
118
+ table.add_column("Cohort", style="bold")
119
+
120
+ for cohort in sorted(cohorts):
121
+ table.add_row(cohort)
122
+
123
+ console.print(table)
124
+
125
+
126
+ def print_cohort_panel(cohort_name: str, members: List[str]) -> None:
127
+ """
128
+ Print detailed cohort info as a rich panel.
129
+ Used by: redis-flags inspect-cohort {cohort_name}
130
+ """
131
+ lines = []
132
+ lines.append(f"[bold]Members[/bold] {len(members)}")
133
+
134
+ if members:
135
+ lines.append("")
136
+ for member in sorted(members):
137
+ lines.append(f" {member}")
138
+ else:
139
+ lines.append("")
140
+ lines.append(" [dim]no members[/dim]")
141
+
142
+ console.print(Panel(
143
+ "\n".join(lines),
144
+ title=f"[bold cyan]{cohort_name}[/bold cyan]",
145
+ expand=False,
146
+ ))
147
+
148
+
149
+ def print_success(message: str) -> None:
150
+ """Print a green success message."""
151
+ console.print(f"[green]✓[/green] {message}")
152
+
153
+
154
+ def print_error(message: str) -> None:
155
+ """Print a red error message."""
156
+ console.print(f"[red]Error:[/red] {message}")
157
+
158
+
159
+ def print_warning(message: str) -> None:
160
+ """Print a yellow warning message."""
161
+ console.print(f"[yellow]Warning:[/yellow] {message}")
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: redis-flags
3
+ Version: 0.1.0
4
+ Summary: CLI for redis-feature-flags — manage feature flags from your terminal.
5
+ License: MIT
6
+ Keywords: cli,feature-flags,redis
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: redis-feature-flags>=0.1.0
9
+ Requires-Dist: redis>=4.0.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: tomli-w
12
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
13
+ Requires-Dist: typer>=0.9.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: fakeredis>=2.0; extra == 'dev'
16
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
17
+ Requires-Dist: pytest>=7.0; extra == 'dev'
18
+ Requires-Dist: typer[testing]>=0.9.0; extra == 'dev'
@@ -0,0 +1,14 @@
1
+ redis_flags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ redis_flags/config.py,sha256=1gutwKeCsjlI_BERyGwrUSRX4vJ54DurAWoCASfRoMo,2425
3
+ redis_flags/connection.py,sha256=umOTKosJPWuM6jzw3EBaQo0U6Uh2rxRqUNsf4KXGd7Q,1480
4
+ redis_flags/main.py,sha256=xnOLeR3ZSNK9g6Y4DX9-Wfn0anzso_n3n_DGUD7mN-o,1977
5
+ redis_flags/output.py,sha256=OmUpuBKQRpyo8FjuDjaIdBT8hWE1ukjWjFdyXyixIJY,4726
6
+ redis_flags/commands/__init__.py,sha256=fRdr-VtmLBr_rfT8F2gpvOPSasPTZ2Db-Z1nu-ijHrQ,41
7
+ redis_flags/commands/cohort.py,sha256=HXQGwhp6jKbXhppgww50TBnGIWp1CIFbKut40zyH9co,4323
8
+ redis_flags/commands/flag.py,sha256=mCUDvjm-hErQrWKZgra_cgfSdSBvvPa9mJbbc5kWUsI,6941
9
+ redis_flags/commands/history.py,sha256=Qe-5gRlkcvzPGbkGJ6n79M0zD5bIOS8DY2NJnWNfz4M,1151
10
+ redis_flags/commands/user.py,sha256=gnu-d9LcJatPwg9HbQ40o0ITt_GysWED2QrdglAUoDw,1946
11
+ redis_flags-0.1.0.dist-info/METADATA,sha256=reUUbmOZ0TYki4Bsj0z1HAP2FOBQp67NHNvqRzfpafg,618
12
+ redis_flags-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ redis_flags-0.1.0.dist-info/entry_points.txt,sha256=OCUxGN3dw1t9WpYdXVffvHsuJ8GDfBBbzfgC2Cone-4,53
14
+ redis_flags-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ redis-flags = redis_flags.main:app