pingera-cli 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.
@@ -0,0 +1,11 @@
1
+ """
2
+ PingeraCLI - A beautiful CLI tool for Pingera Platform
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "PingeraCLI Team"
7
+ __description__ = "A beautiful Python CLI tool built with typer and rich, distributed via pip and based on Pingera SDK"
8
+
9
+ from .main import app
10
+
11
+ __all__ = ["app", "__version__"]
@@ -0,0 +1,9 @@
1
+
2
+ """
3
+ Main module entry point for python -m pingera_cli execution
4
+ """
5
+
6
+ from .main import cli_entry_point
7
+
8
+ if __name__ == "__main__":
9
+ cli_entry_point()
@@ -0,0 +1,11 @@
1
+ """
2
+ Commands module for PingeraCLI
3
+ """
4
+
5
+ from .auth import auth_cmd
6
+
7
+ __all__ = ["auth_cmd"]
8
+
9
+ from .base import BaseCommand
10
+
11
+ __all__ = ["BaseCommand"]
@@ -0,0 +1,214 @@
1
+ """
2
+ Authentication commands for PingeraCLI
3
+ """
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.panel import Panel
10
+ from rich.prompt import Prompt
11
+
12
+ from .base import BaseCommand
13
+ from ..utils.config import get_config, save_config, get_api_key, set_api_key
14
+
15
+
16
+ class AuthCommand(BaseCommand):
17
+ """
18
+ Authentication command handler
19
+ """
20
+
21
+ def __init__(self):
22
+ super().__init__()
23
+ self.app = typer.Typer(
24
+ name="auth",
25
+ help="🔐 Manage authentication settings",
26
+ no_args_is_help=True,
27
+ )
28
+
29
+ # Register commands
30
+ self.app.command("login")(self.login)
31
+ self.app.command("status")(self.status)
32
+ self.app.command("logout")(self.logout)
33
+
34
+ def login(
35
+ self,
36
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="Pingera API key"),
37
+ interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive mode"),
38
+ ):
39
+ """
40
+ Login to Pingera by setting API key
41
+ """
42
+ try:
43
+ # If no API key provided, empty, or whitespace-only, or interactive mode requested, prompt for it
44
+ if not api_key or not api_key.strip() or interactive:
45
+ try:
46
+ api_key = Prompt.ask(
47
+ "[bold blue]Enter your Pingera API key[/bold blue]",
48
+ password=True
49
+ )
50
+ except KeyboardInterrupt:
51
+ self.console.print("\n[yellow]⚠ Operation cancelled[/yellow]")
52
+ raise typer.Exit(1)
53
+
54
+ # Validate API key after potentially getting it from prompt
55
+ if not api_key or not api_key.strip():
56
+ self.console.print("[red]API key cannot be empty[/red]")
57
+ raise typer.Exit(1)
58
+
59
+ if len(api_key.strip()) < 10:
60
+ self.display_warning(
61
+ "API key seems too short. Please verify it's correct.",
62
+ "⚠️ Warning"
63
+ )
64
+
65
+ # Test the API key by making a simple request
66
+ self.console.print("🔍 Validating API key...")
67
+
68
+ # For now, we'll skip API validation since we need to implement the client first
69
+ # TODO: Add actual API key validation with Pingera SDK
70
+
71
+ # Save API key to config
72
+ if set_api_key(api_key.strip()):
73
+ self.display_success(
74
+ f"✅ API key has been saved successfully!\n\n"
75
+ f"You can now use all Pingera CLI commands.\n"
76
+ f"Run [cyan]pngr auth status[/cyan] to verify your authentication.",
77
+ "🔐 Authentication Success"
78
+ )
79
+ else:
80
+ self.console.print("[red]Failed to save API key to configuration file.[/red]")
81
+ raise typer.Exit(1)
82
+
83
+ except Exception as e:
84
+ self.display_error(f"Login failed: {str(e)}")
85
+ raise typer.Exit(1)
86
+
87
+ def status(self):
88
+ """
89
+ Check current authentication status
90
+ """
91
+ try:
92
+ # Check for API key in environment variable
93
+ env_api_key = os.getenv('PINGERA_API_KEY')
94
+
95
+ # Check for API key in config file
96
+ config_api_key = get_api_key()
97
+
98
+ # Determine which API key is being used
99
+ active_api_key = env_api_key or config_api_key
100
+
101
+ if active_api_key:
102
+ # Mask the API key for display
103
+ masked_key = active_api_key[:8] + "..." + active_api_key[-4:] if len(active_api_key) > 12 else "***"
104
+
105
+ source = "environment variable" if env_api_key else "config file"
106
+
107
+ status_content = f"""
108
+ [green]✅ Authenticated[/green]
109
+
110
+ API Key: ✓ Set ({masked_key})
111
+ Source: {source}
112
+ Status: [green]Active[/green]
113
+
114
+ [dim]Note: Environment variable takes precedence over config file.[/dim]
115
+ """
116
+
117
+ # TODO: Add API key validation request here
118
+ # Example:
119
+ # try:
120
+ # # Test API key with a simple request
121
+ # client = PingeraClient.from_api_key(active_api_key)
122
+ # # Make a test request
123
+ # status_content += "\n[bold]API Status:[/bold] [green]✅ Valid[/green]"
124
+ # except Exception as e:
125
+ # status_content += f"\n[bold]API Status:[/bold] [red]❌ Invalid ({str(e)})[/red]"
126
+
127
+ else:
128
+ status_content = """
129
+ [red]❌ Not Authenticated[/red]
130
+
131
+ API Key: ✗ Not set
132
+
133
+ [bold]To authenticate:[/bold]
134
+ • Run [cyan]pngr auth login --api-key YOUR_KEY[/cyan]
135
+ • Or run [cyan]pngr auth login --interactive[/cyan]
136
+ • Or set environment variable: [cyan]export PINGERA_API_KEY=your_key[/cyan]
137
+ """
138
+
139
+ panel = Panel(
140
+ status_content.strip(),
141
+ title="🔐 Authentication Status",
142
+ border_style="green" if active_api_key else "red",
143
+ padding=(1, 2),
144
+ )
145
+
146
+ self.console.print(panel)
147
+
148
+ except Exception as e:
149
+ self.display_error(f"Failed to check authentication status: {str(e)}")
150
+ raise typer.Exit(1)
151
+
152
+ def logout(
153
+ self,
154
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"),
155
+ ):
156
+ """
157
+ Clear stored credentials
158
+ """
159
+ try:
160
+ # Check if there's anything to logout from
161
+ config_api_key = get_api_key()
162
+ env_api_key = os.getenv('PINGERA_API_KEY')
163
+
164
+ if not config_api_key and not env_api_key:
165
+ self.display_info(
166
+ "No stored credentials found. You are already logged out.",
167
+ "ℹ️ Info"
168
+ )
169
+ return
170
+
171
+ # Warning about environment variable
172
+ if env_api_key and not confirm:
173
+ self.display_warning(
174
+ "API key is set via environment variable (PINGERA_API_KEY).\n"
175
+ "This command will only clear the config file.\n"
176
+ "To fully logout, also run: [cyan]unset PINGERA_API_KEY[/cyan]",
177
+ "⚠️ Environment Variable Detected"
178
+ )
179
+
180
+ # Confirmation prompt
181
+ if not confirm:
182
+ should_logout = self.prompt_confirmation(
183
+ "Are you sure you want to clear stored credentials?",
184
+ default=False
185
+ )
186
+ if not should_logout:
187
+ self.console.print("[yellow]Logout cancelled[/yellow]")
188
+ return
189
+
190
+ # Clear API key from config
191
+ config = get_config()
192
+ if 'api_key' in config:
193
+ del config['api_key']
194
+ if save_config(config):
195
+ self.display_success(
196
+ "✅ Credentials cleared from configuration file.\n\n"
197
+ + ("Note: Environment variable PINGERA_API_KEY is still set." if env_api_key else "You have been logged out successfully."),
198
+ "🚪 Logout Success"
199
+ )
200
+ else:
201
+ self.display_error("Failed to clear credentials from configuration file.")
202
+ raise typer.Exit(1)
203
+ else:
204
+ self.display_info("No credentials found in configuration file.")
205
+
206
+ except KeyboardInterrupt:
207
+ self.console.print("\n[yellow]⚠ Operation cancelled[/yellow]")
208
+ except Exception as e:
209
+ self.display_error(f"Logout failed: {str(e)}")
210
+ raise typer.Exit(1)
211
+
212
+
213
+ # Create the auth command instance
214
+ auth_cmd = AuthCommand()
@@ -0,0 +1,251 @@
1
+ """
2
+ Base command class for PingeraCLI commands
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Dict, Optional
7
+ from datetime import datetime
8
+
9
+ try:
10
+ import yaml
11
+ except ImportError:
12
+ yaml = None
13
+
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+
18
+ from ..utils.console import console, error_console
19
+ from ..utils.config import get_config
20
+
21
+
22
+ class BaseCommand:
23
+ """Base class for all CLI commands with common functionality"""
24
+
25
+ def __init__(self, output_format: Optional[str] = None):
26
+ self.console = console
27
+ self.error_console = error_console
28
+ self.output_format = output_format or get_config().get('output_format', 'table')
29
+
30
+ def validate_api_key(self) -> str:
31
+ """
32
+ Validate that API key is available
33
+
34
+ Returns:
35
+ str: The API key
36
+
37
+ Raises:
38
+ ValueError: If API key is not found
39
+ """
40
+ api_key = os.getenv('PINGERA_API_KEY')
41
+ if not api_key:
42
+ raise ValueError("PINGERA_API_KEY environment variable not set")
43
+ return api_key
44
+
45
+ def validate_pingera_sdk(self):
46
+ """
47
+ Validate that Pingera SDK is installed and importable
48
+
49
+ Raises:
50
+ ImportError: If Pingera SDK is not available
51
+ """
52
+ try:
53
+ from pingera import ApiClient, Configuration
54
+ return (ApiClient, Configuration)
55
+ except ImportError:
56
+ raise ImportError("Pingera SDK not installed. Install with: pip install pingera-sdk")
57
+
58
+ def display_success(self, message: str, title: str = "✅ Success"):
59
+ """
60
+ Display a success message in a panel
61
+
62
+ Args:
63
+ message: The success message to display
64
+ title: Panel title (default: "✅ Success")
65
+ """
66
+ panel = Panel(
67
+ message,
68
+ title=title,
69
+ border_style="green",
70
+ padding=(1, 2),
71
+ )
72
+ self.console.print(panel)
73
+
74
+ def display_error(self, message: str, title: str = "❌ Error"):
75
+ """
76
+ Display an error message in a panel
77
+
78
+ Args:
79
+ message: The error message to display
80
+ title: Panel title (default: "❌ Error")
81
+ """
82
+ panel = Panel(
83
+ message,
84
+ title=title,
85
+ border_style="red",
86
+ padding=(1, 2),
87
+ )
88
+ self.error_console.print(panel)
89
+
90
+ def display_warning(self, message: str, title: str = "⚠️ Warning"):
91
+ """Display a warning message in a panel"""
92
+ panel = Panel(
93
+ f"[yellow]{message}[/yellow]",
94
+ title=title,
95
+ border_style="yellow",
96
+ padding=(1, 2),
97
+ )
98
+ self.console.print(panel)
99
+
100
+ def display_info(self, message: str, title: str = "ℹ️ Info"):
101
+ """Display an info message in a panel"""
102
+ panel = Panel(
103
+ f"[blue]{message}[/blue]",
104
+ title=title,
105
+ border_style="blue",
106
+ padding=(1, 2),
107
+ )
108
+ self.console.print(panel)
109
+
110
+ def create_table(self, title: str, columns: list, rows: list) -> Table:
111
+ """
112
+ Create a rich table with the specified columns and rows
113
+
114
+ Args:
115
+ title: Table title
116
+ columns: List of column names
117
+ rows: List of row data (each row is a list)
118
+
119
+ Returns:
120
+ Table: Configured rich Table object
121
+ """
122
+ table = Table(title=title, show_header=True, header_style="bold magenta")
123
+
124
+ # Add columns
125
+ for column in columns:
126
+ table.add_column(column, style="cyan", no_wrap=False)
127
+
128
+ # Add rows
129
+ for row in rows:
130
+ table.add_row(*[str(cell) for cell in row])
131
+
132
+ return table
133
+
134
+ def display_table(self, title: str, columns: list, rows: list):
135
+ """
136
+ Display a table using rich formatting
137
+
138
+ Args:
139
+ title: Table title
140
+ columns: List of column names
141
+ rows: List of row data (each row is a list)
142
+ """
143
+ table = self.create_table(title, columns, rows)
144
+ self.console.print(table)
145
+
146
+ def prompt_confirmation(self, message: str, default: bool = False) -> bool:
147
+ """
148
+ Prompt user for confirmation
149
+
150
+ Args:
151
+ message: The confirmation message
152
+ default: Default value if user just presses enter
153
+
154
+ Returns:
155
+ bool: True if confirmed, False otherwise
156
+ """
157
+ default_text = "Y/n" if default else "y/N"
158
+ response = input(f"{message} [{default_text}]: ").lower().strip()
159
+
160
+ if not response:
161
+ return default
162
+
163
+ return response in ['y', 'yes', '1', 'true']
164
+
165
+ def output_data(self, data: Any, format_override: Optional[str] = None):
166
+ """Output data in the specified format (table, json, yaml)"""
167
+ output_format = format_override or self.output_format
168
+
169
+ if output_format == 'json':
170
+ from datetime import datetime
171
+
172
+ def json_serializer(obj):
173
+ """JSON serializer for datetime objects"""
174
+ if isinstance(obj, datetime):
175
+ return obj.isoformat()
176
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
177
+
178
+ self.console.print(json.dumps(data, indent=2, default=json_serializer))
179
+ elif output_format == 'yaml':
180
+ if yaml is None:
181
+ self.console.print("[yellow]YAML support not available. Install with: pip install pyyaml[/yellow]")
182
+ from datetime import datetime
183
+
184
+ def json_serializer(obj):
185
+ """JSON serializer for datetime objects"""
186
+ if isinstance(obj, datetime):
187
+ return obj.isoformat()
188
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
189
+
190
+ self.console.print(json.dumps(data, indent=2, default=json_serializer))
191
+ else:
192
+ try:
193
+ self.console.print(yaml.dump(data, default_flow_style=False))
194
+ except Exception:
195
+ # Fallback to JSON if YAML fails
196
+ from datetime import datetime
197
+
198
+ def json_serializer(obj):
199
+ """JSON serializer for datetime objects"""
200
+ if isinstance(obj, datetime):
201
+ return obj.isoformat()
202
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
203
+
204
+ self.console.print(json.dumps(data, indent=2, default=json_serializer))
205
+ else:
206
+ # Default to table format - subclasses should override this
207
+ if isinstance(data, dict):
208
+ self._display_dict_as_table(data)
209
+ elif isinstance(data, list):
210
+ self._display_list_as_table(data)
211
+ else:
212
+ self.console.print(str(data))
213
+
214
+ def _display_dict_as_table(self, data: Dict[str, Any]):
215
+ """Display dictionary data as a table"""
216
+ table = Table(title="Data")
217
+ table.add_column("Key", style="cyan")
218
+ table.add_column("Value", style="white")
219
+
220
+ for key, value in data.items():
221
+ table.add_row(str(key), str(value))
222
+
223
+ self.console.print(table)
224
+
225
+ def _display_list_as_table(self, data: list):
226
+ """Display list data as a table"""
227
+ if not data:
228
+ self.console.print("No data available")
229
+ return
230
+
231
+ if isinstance(data[0], dict):
232
+ # Create table from list of dictionaries
233
+ table = Table(title="Data")
234
+ if data:
235
+ for key in data[0].keys():
236
+ table.add_column(str(key).replace('_', ' ').title())
237
+
238
+ for item in data:
239
+ row = [str(item.get(key, '')) for key in data[0].keys()]
240
+ table.add_row(*row)
241
+
242
+ self.console.print(table)
243
+ else:
244
+ # Simple list
245
+ table = Table(title="Data")
246
+ table.add_column("Item", style="white")
247
+
248
+ for item in data:
249
+ table.add_row(str(item))
250
+
251
+ self.console.print(table)