redis-flags 0.1.0__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.
Binary file
@@ -0,0 +1 @@
1
+ ~/.redis-flags.toml
@@ -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'
File without changes
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "redis-flags"
7
+ version = "0.1.0"
8
+ description = "CLI for redis-feature-flags — manage feature flags from your terminal."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["redis", "feature-flags", "cli"]
13
+ dependencies = [
14
+ "redis>=4.0.0",
15
+ "typer>=0.9.0",
16
+ "rich>=13.0.0",
17
+ "tomli>=2.0.0; python_version < '3.11'",
18
+ "tomli-w",
19
+ "redis-feature-flags>=0.1.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ redis-flags = "redis_flags.main:app"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["redis_flags"]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0",
31
+ "pytest-cov>=4.0",
32
+ "fakeredis>=2.0",
33
+ "typer[testing]>=0.9.0",
34
+ ]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+ addopts = "--cov=redis_flags --cov-report=term-missing --cov-fail-under=90"
39
+
40
+ [tool.coverage.run]
41
+ source = ["redis_flags"]
42
+ omit = ["tests/*"]
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]")
@@ -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)