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.
- pingera_cli/__init__.py +11 -0
- pingera_cli/__main__.py +9 -0
- pingera_cli/commands/__init__.py +11 -0
- pingera_cli/commands/auth.py +214 -0
- pingera_cli/commands/base.py +251 -0
- pingera_cli/commands/checks.py +1011 -0
- pingera_cli/commands/on_demand_checks.py +646 -0
- pingera_cli/formatters/__init__.py +13 -0
- pingera_cli/formatters/base_formatter.py +45 -0
- pingera_cli/formatters/generic_formatter.py +51 -0
- pingera_cli/formatters/multistep_formatter.py +141 -0
- pingera_cli/formatters/registry.py +35 -0
- pingera_cli/formatters/ssl_formatter.py +104 -0
- pingera_cli/formatters/synthetic_formatter.py +350 -0
- pingera_cli/formatters/web_formatter.py +78 -0
- pingera_cli/main.py +214 -0
- pingera_cli/utils/__init__.py +8 -0
- pingera_cli/utils/config.py +187 -0
- pingera_cli/utils/console.py +87 -0
- pingera_cli-0.1.0.dist-info/METADATA +199 -0
- pingera_cli-0.1.0.dist-info/RECORD +25 -0
- pingera_cli-0.1.0.dist-info/WHEEL +5 -0
- pingera_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pingera_cli-0.1.0.dist-info/licenses/LICENSE +22 -0
- pingera_cli-0.1.0.dist-info/top_level.txt +1 -0
pingera_cli/__init__.py
ADDED
|
@@ -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__"]
|
pingera_cli/__main__.py
ADDED
|
@@ -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)
|