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,350 @@
1
+ """
2
+ Synthetic/Browser check result formatter
3
+ """
4
+
5
+ from typing import Dict, Any, List
6
+ from .base_formatter import BaseFormatter
7
+
8
+
9
+ class SyntheticFormatter(BaseFormatter):
10
+ """Formatter for synthetic/browser check results"""
11
+
12
+ def can_format(self, metadata: Dict[str, Any]) -> bool:
13
+ """Check if this is synthetic metadata"""
14
+ return 'execution_time' in metadata and 'pages' in metadata
15
+
16
+ def format(self, metadata: Dict[str, Any]) -> str:
17
+ """Format synthetic check metadata"""
18
+ info = "\n[bold cyan]Synthetic Check Results:[/bold cyan]"
19
+
20
+ # Execution time
21
+ if 'execution_time' in metadata:
22
+ info += f"\n• Total Execution Time: [yellow]{metadata['execution_time']}ms[/yellow]"
23
+
24
+ # Test Summary
25
+ if 'test_summary' in metadata:
26
+ info += self._format_test_summary(metadata['test_summary'])
27
+
28
+ # Test Results
29
+ if 'test_results' in metadata:
30
+ info += self._format_test_results(metadata['test_results'])
31
+
32
+ # Logs
33
+ if 'logs' in metadata and metadata['logs']:
34
+ info += self._format_logs(metadata['logs'])
35
+
36
+ # Page Performance
37
+ if 'pages' in metadata and metadata['pages']:
38
+ info += self._format_page_performance(metadata['pages'][0])
39
+
40
+ return info
41
+
42
+ def _format_test_summary(self, summary: Dict[str, Any]) -> str:
43
+ """Format test summary"""
44
+ total = summary.get('total', 0)
45
+ passed = summary.get('passed', 0)
46
+ failed = summary.get('failed', 0)
47
+ return f"\n• Tests: [green]{passed} passed[/green], [red]{failed} failed[/red], [white]{total} total[/white]"
48
+
49
+ def _format_test_results(self, test_results: List[Dict[str, Any]]) -> str:
50
+ """Format test results"""
51
+ info = "\n\n[bold cyan]Test Results:[/bold cyan]"
52
+
53
+ for test in test_results:
54
+ status_color = "green" if test.get('status') == 'passed' else "red"
55
+ status_icon = "✅" if test.get('status') == 'passed' else "❌"
56
+ info += f"\n• {test.get('name', 'Unnamed Test')}: [{status_color}]{status_icon} {test.get('status', 'unknown')}[/{status_color}]"
57
+
58
+ if test.get('duration'):
59
+ info += f" ([yellow]{test['duration']}ms[/yellow])"
60
+ if test.get('error'):
61
+ info += f"\n [red]Error: {test['error']}[/red]"
62
+
63
+ # Browser Screenshots/Videos
64
+ if 'browser_metadata' in test:
65
+ info += self._format_browser_metadata(test['browser_metadata'])
66
+
67
+ return info
68
+
69
+ def _format_browser_metadata(self, browser_meta: Dict[str, Any]) -> str:
70
+ """Format browser metadata (screenshots, videos)"""
71
+ info = ""
72
+
73
+ if browser_meta.get('screenshots'):
74
+ info += f"\n• Screenshots: [blue]{len(browser_meta['screenshots'])} available[/blue]"
75
+ screenshot_limit = None if self.verbose else 3
76
+ for screenshot in browser_meta['screenshots'][:screenshot_limit]:
77
+ name = screenshot.get('name', 'screenshot')
78
+ # Add download link if available
79
+ if 'url' in screenshot:
80
+ info += f"\n - [dim]{name}[/dim] - [blue]{screenshot['url']}[/blue]"
81
+ elif 'download_url' in screenshot:
82
+ info += f"\n - [dim]{name}[/dim] - [blue]{screenshot['download_url']}[/blue]"
83
+ else:
84
+ info += f"\n - [dim]{name}[/dim]"
85
+ if not self.verbose and len(browser_meta['screenshots']) > 3:
86
+ info += f"\n - [dim]... and {len(browser_meta['screenshots']) - 3} more (use --verbose to see all)[/dim]"
87
+
88
+ if browser_meta.get('videos'):
89
+ info += f"\n• Videos: [blue]{len(browser_meta['videos'])} available[/blue]"
90
+ if self.verbose:
91
+ for video in browser_meta['videos']:
92
+ name = video.get('name', 'video')
93
+ if 'url' in video:
94
+ info += f"\n - [dim]{name}[/dim] - [blue]{video['url']}[/blue]"
95
+ elif 'download_url' in video:
96
+ info += f"\n - [dim]{name}[/dim] - [blue]{video['download_url']}[/blue]"
97
+ else:
98
+ info += f"\n - [dim]{name}[/dim]"
99
+
100
+ return info
101
+
102
+ def _format_logs(self, logs: List[Dict[str, Any]]) -> str:
103
+ """Format execution logs"""
104
+ info = "\n\n[bold cyan]Execution Logs:[/bold cyan]"
105
+ log_limit = None if self.verbose else 5
106
+
107
+ for i, log in enumerate(logs[:log_limit]):
108
+ level = log.get('level', 'info')
109
+ level_color = {"error": "red", "warn": "yellow", "info": "blue", "log": "white"}.get(level, "white")
110
+ timestamp = log.get('timestamp', 'Unknown time')
111
+ message = log.get('message', 'No message')
112
+ info += f"\n• [{level_color}]{level.upper()}[/{level_color}] [{timestamp}]: {message}"
113
+
114
+ if not self.verbose and len(logs) > 5:
115
+ info += f"\n• [dim]... and {len(logs) - 5} more logs (use --verbose to see all)[/dim]"
116
+
117
+ return info
118
+
119
+ def _format_page_performance(self, page: Dict[str, Any]) -> str:
120
+ """Format page performance data"""
121
+ info = "\n\n[bold cyan]Page Performance:[/bold cyan]"
122
+
123
+ if 'duration' in page:
124
+ info += f"\n• Page Load Duration: [yellow]{page['duration']:.2f}ms[/yellow]"
125
+
126
+ # Web Vitals
127
+ if 'webVitals' in page:
128
+ info += self._format_web_vitals(page['webVitals'])
129
+
130
+ # Network requests
131
+ if 'network' in page:
132
+ info += self._format_network_requests(page['network'])
133
+
134
+ # Document request details
135
+ if 'documentRequest' in page:
136
+ info += self._format_document_request(page['documentRequest'])
137
+
138
+ return info
139
+
140
+ def _format_web_vitals(self, vitals: Dict[str, Any]) -> str:
141
+ """Format Web Vitals data"""
142
+ info = "\n• Web Vitals:"
143
+ if 'FCP' in vitals:
144
+ info += f"\n - First Contentful Paint: [yellow]{vitals['FCP']}ms[/yellow]"
145
+ if 'LCP' in vitals:
146
+ info += f"\n - Largest Contentful Paint: [yellow]{vitals['LCP']}ms[/yellow]"
147
+ if 'TTFB' in vitals:
148
+ info += f"\n - Time to First Byte: [yellow]{vitals['TTFB']}ms[/yellow]"
149
+ if self.verbose and 'CLS' in vitals:
150
+ info += f"\n - Cumulative Layout Shift: [yellow]{vitals['CLS']}[/yellow]"
151
+ if self.verbose and 'FID' in vitals:
152
+ info += f"\n - First Input Delay: [yellow]{vitals['FID']}ms[/yellow]"
153
+ return info
154
+
155
+ def _format_network_requests(self, network: List[Dict[str, Any]]) -> str:
156
+ """Format network requests"""
157
+ info = f"\n• Network Requests: [white]{len(network)} total[/white]"
158
+
159
+ # Summarize by resource type
160
+ resource_types = {}
161
+ for req in network:
162
+ res_type = req.get('resourceType', 'unknown')
163
+ resource_types[res_type] = resource_types.get(res_type, 0) + 1
164
+
165
+ for res_type, count in resource_types.items():
166
+ info += f"\n - {res_type}: [blue]{count}[/blue]"
167
+
168
+ # Show detailed network requests in verbose mode
169
+ if self.verbose:
170
+ info += self._format_detailed_network_requests(network)
171
+
172
+ return info
173
+
174
+ def _format_detailed_network_requests(self, network: List[Dict[str, Any]]) -> str:
175
+ """Format detailed network requests (verbose mode)"""
176
+ info = "\n\n[bold cyan]Detailed Network Requests:[/bold cyan]"
177
+
178
+ # In verbose mode, show ALL requests, not just first 20
179
+ limit = len(network) if self.verbose else 20
180
+
181
+ for i, req in enumerate(network[:limit]):
182
+ url = req.get('url', 'Unknown URL')
183
+ method = req.get('method', 'GET')
184
+
185
+ # Extract status from response field with better logic
186
+ status = self._extract_status(req)
187
+ size = self._extract_size(req)
188
+ duration = self._extract_duration(req)
189
+ res_type = req.get('resourceType', 'unknown')
190
+
191
+ # Color code status
192
+ status_display = self._format_status_code(status)
193
+ size_display = self._format_size(size)
194
+ duration_display = self._format_duration(duration)
195
+
196
+ info += f"\n• [{method}] {status_display} [{res_type}] {size_display} {duration_display}"
197
+ info += self._format_url_with_breaks(url)
198
+
199
+ # Only show truncation message if NOT in verbose mode
200
+ if not self.verbose and len(network) > 20:
201
+ info += f"\n• [dim]... and {len(network) - 20} more requests (use --verbose to see all)[/dim]"
202
+
203
+ return info
204
+
205
+ def _extract_status(self, req: Dict[str, Any]) -> Any:
206
+ """Extract status code from request with improved logic"""
207
+ # Try multiple possible locations for status code
208
+ if 'response' in req and req['response'] and isinstance(req['response'], dict):
209
+ response = req['response']
210
+ if 'status' in response:
211
+ return response['status']
212
+ if 'statusCode' in response:
213
+ return response['statusCode']
214
+
215
+ # Try direct status field
216
+ if 'status' in req:
217
+ return req['status']
218
+ if 'statusCode' in req:
219
+ return req['statusCode']
220
+
221
+ # Try nested in other common locations
222
+ if 'networkResponse' in req and req['networkResponse']:
223
+ net_resp = req['networkResponse']
224
+ if 'status' in net_resp:
225
+ return net_resp['status']
226
+
227
+ # For failed requests, they might not have a response
228
+ if req.get('failed') or req.get('errorText'):
229
+ return 'Failed'
230
+
231
+ return 'Unknown'
232
+
233
+ def _extract_size(self, req: Dict[str, Any]) -> int:
234
+ """Extract size from request with improved logic"""
235
+ # Try multiple possible locations for size data
236
+ if 'response' in req and req['response'] and isinstance(req['response'], dict):
237
+ response_data = req['response']
238
+
239
+ # Try different size fields in order of preference
240
+ for size_field in ['encodedDataLength', 'transferSize', 'decodedBodyLength', 'contentLength']:
241
+ if size_field in response_data and response_data[size_field] is not None:
242
+ return int(response_data[size_field])
243
+
244
+ # Try direct size fields
245
+ for size_field in ['encodedDataLength', 'transferSize', 'size', 'contentLength']:
246
+ if size_field in req and req[size_field] is not None:
247
+ return int(req[size_field])
248
+
249
+ return 0
250
+
251
+ def _extract_duration(self, req: Dict[str, Any]) -> float:
252
+ """Extract duration from request with improved logic"""
253
+ # Try direct duration field first
254
+ if 'duration' in req and req['duration'] is not None and req['duration'] > 0:
255
+ return float(req['duration'])
256
+
257
+ # Calculate from timing if available
258
+ if 'timing' in req and req['timing']:
259
+ timing = req['timing']
260
+ if 'responseEnd' in timing and 'requestStart' in timing:
261
+ duration = timing['responseEnd'] - timing['requestStart']
262
+ if duration > 0:
263
+ return duration
264
+
265
+ # Try other common duration fields
266
+ for duration_field in ['responseTime', 'loadTime', 'totalTime']:
267
+ if duration_field in req and req[duration_field] is not None:
268
+ return float(req[duration_field])
269
+
270
+ return 0
271
+
272
+ def _format_status_code(self, status: Any) -> str:
273
+ """Format status code with appropriate color"""
274
+ if isinstance(status, int):
275
+ if 200 <= status < 300:
276
+ return f"[green]{status}[/green]"
277
+ elif 400 <= status < 500:
278
+ return f"[yellow]{status}[/yellow]"
279
+ elif status >= 500:
280
+ return f"[red]{status}[/red]"
281
+ else:
282
+ return f"[white]{status}[/white]"
283
+ elif str(status).lower() == 'failed':
284
+ return f"[red]Failed[/red]"
285
+ else:
286
+ return f"[white]{status}[/white]"
287
+
288
+ def _format_url_with_breaks(self, url: str) -> str:
289
+ """Format URL with line breaks for readability"""
290
+ if len(url) <= 90:
291
+ return f"\n [dim]{url}[/dim]"
292
+
293
+ # Find a good break point
294
+ break_points = ['/', '?', '&', '=']
295
+ best_break = 80
296
+ for bp in break_points:
297
+ pos = url.rfind(bp, 50, 85)
298
+ if pos > 0:
299
+ best_break = pos + 1
300
+ break
301
+
302
+ return f"\n [dim]{url[:best_break]}[/dim]\n [dim]{url[best_break:]}[/dim]"
303
+
304
+ def _format_document_request(self, doc_req: Dict[str, Any]) -> str:
305
+ """Format main document request details"""
306
+ info = "\n\n[bold cyan]Main Document Request:[/bold cyan]"
307
+
308
+ if 'url' in doc_req:
309
+ info += f"\n• URL: [white]{doc_req['url']}[/white]"
310
+ if 'method' in doc_req:
311
+ info += f"\n• Method: [white]{doc_req['method']}[/white]"
312
+ if 'statusCode' in doc_req:
313
+ status_code = doc_req['statusCode']
314
+ if 200 <= status_code < 300:
315
+ info += f"\n• Status: [green]{status_code}[/green]"
316
+ elif 400 <= status_code < 500:
317
+ info += f"\n• Status: [yellow]{status_code}[/yellow]"
318
+ else:
319
+ info += f"\n• Status: [red]{status_code}[/red]"
320
+
321
+ if self.verbose and 'responseHeaders' in doc_req:
322
+ info += self._format_response_headers(doc_req['responseHeaders'])
323
+
324
+ return info
325
+
326
+ def _format_response_headers(self, headers: Dict[str, Any]) -> str:
327
+ """Format response headers"""
328
+ info = "\n• Response Headers:"
329
+
330
+ if self.verbose:
331
+ # In verbose mode, show ALL headers
332
+ for header, value in headers.items():
333
+ info += f"\n - {header}: [dim]{value}[/dim]"
334
+ else:
335
+ # In non-verbose mode, show important headers only
336
+ important_headers = [
337
+ 'content-type', 'content-length', 'server', 'cache-control',
338
+ 'x-frame-options', 'strict-transport-security'
339
+ ]
340
+
341
+ for header in important_headers:
342
+ if header in headers:
343
+ value = self._truncate_text(headers[header])
344
+ info += f"\n - {header}: [dim]{value}[/dim]"
345
+
346
+ other_count = len(headers) - len([h for h in important_headers if h in headers])
347
+ if other_count > 0:
348
+ info += f"\n - [dim]... and {other_count} more headers[/dim]"
349
+
350
+ return info
@@ -0,0 +1,78 @@
1
+
2
+ """
3
+ Web check result formatter
4
+ """
5
+
6
+ from typing import Dict, Any
7
+ from .base_formatter import BaseFormatter
8
+
9
+
10
+ class WebFormatter(BaseFormatter):
11
+ """Formatter for web check results"""
12
+
13
+ def can_format(self, metadata: Dict[str, Any]) -> bool:
14
+ """Check if this is web metadata"""
15
+ return 'headers' in metadata or 'status_code' in metadata
16
+
17
+ def format(self, metadata: Dict[str, Any]) -> str:
18
+ """Format web check metadata"""
19
+ info = "\n[bold cyan]Web Check Results:[/bold cyan]"
20
+
21
+ # HTTP Response
22
+ if 'status_code' in metadata:
23
+ code = metadata['status_code']
24
+ if 200 <= code < 300:
25
+ info += f"\n• Status Code: [green]{code}[/green]"
26
+ elif 400 <= code < 500:
27
+ info += f"\n• Status Code: [yellow]{code}[/yellow]"
28
+ else:
29
+ info += f"\n• Status Code: [red]{code}[/red]"
30
+
31
+ # IP and Location
32
+ if 'ip_address' in metadata:
33
+ info += f"\n• IP Address: [white]{metadata['ip_address']}[/white]"
34
+ if 'region' in metadata:
35
+ info += f"\n• Region: [white]{metadata['region']}[/white]"
36
+ if 'provider' in metadata:
37
+ info += f"\n• Provider: [white]{metadata['provider']}[/white]"
38
+
39
+ # SSL Certificate Info
40
+ if 'ssl_cert_expiration' in metadata:
41
+ info += f"\n• SSL Expires: [white]{metadata['ssl_cert_expiration']}[/white]"
42
+ if 'ssl_cert_expiration_seconds' in metadata:
43
+ days = metadata['ssl_cert_expiration_seconds'] // 86400
44
+ color = "green" if days > 30 else "yellow" if days > 7 else "red"
45
+ info += f" ([{color}]{days} days remaining[/{color}])"
46
+
47
+ # Response Headers
48
+ if 'headers' in metadata:
49
+ info += self._format_headers(metadata['headers'])
50
+
51
+ return info
52
+
53
+ def _format_headers(self, headers: Dict[str, Any]) -> str:
54
+ """Format response headers"""
55
+ info = "\n\n[bold cyan]Response Headers:[/bold cyan]"
56
+
57
+ if self.verbose:
58
+ # In verbose mode, show ALL headers
59
+ for header, value in headers.items():
60
+ info += f"\n• {header.title()}: [dim]{value}[/dim]"
61
+ else:
62
+ # In non-verbose mode, show important headers only
63
+ important_headers = [
64
+ 'content-type', 'content-length', 'server', 'cache-control',
65
+ 'x-frame-options', 'strict-transport-security', 'x-content-type-options'
66
+ ]
67
+
68
+ for header in important_headers:
69
+ if header in headers:
70
+ value = self._truncate_text(headers[header])
71
+ info += f"\n• {header.title()}: [dim]{value}[/dim]"
72
+
73
+ # Count other headers
74
+ other_count = len(headers) - len([h for h in important_headers if h in headers])
75
+ if other_count > 0:
76
+ info += f"\n• [dim]... and {other_count} more headers (use --verbose to see all)[/dim]"
77
+
78
+ return info
pingera_cli/main.py ADDED
@@ -0,0 +1,214 @@
1
+ """
2
+ Main CLI application entry point using typer framework
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+
14
+ from . import __version__
15
+ from .commands.base import BaseCommand
16
+ from .commands.auth import auth_cmd
17
+ from .commands.checks import app as checks_app
18
+ from .utils.console import console, error_console
19
+ from .utils.config import get_config
20
+
21
+ # Initialize the main Typer app
22
+ app = typer.Typer(
23
+ name="pngr",
24
+ help="🚀 pngr - a nice CLI for Pingera platform\n\n🌐 Web application: https://app.pingera.ru\n💡 Get the API key in the app first to use the CLI",
25
+ rich_markup_mode="rich",
26
+ no_args_is_help=True,
27
+ add_completion=False,
28
+ )
29
+
30
+ # Initialize base command handler
31
+ base_cmd = BaseCommand()
32
+
33
+ # Add subcommands
34
+ app.add_typer(auth_cmd.app, name="auth")
35
+ app.add_typer(checks_app, name="checks")
36
+
37
+
38
+ @app.command("version")
39
+ def version():
40
+ """
41
+ Show pngr version information
42
+ """
43
+ version_text = Text(f"PingeraCLI v{__version__}", style="bold blue")
44
+
45
+ panel = Panel(
46
+ version_text,
47
+ title="🔍 Version Info",
48
+ border_style="blue",
49
+ padding=(1, 2),
50
+ )
51
+
52
+ console.print(panel)
53
+
54
+ # Additional system info
55
+ console.print(f"[dim]Python: {sys.version.split()[0]}[/dim]")
56
+ console.print(f"[dim]Platform: {sys.platform}[/dim]")
57
+
58
+
59
+ @app.command("info")
60
+ def info():
61
+ """
62
+ Show information about pngr and Pingera SDK
63
+ """
64
+ try:
65
+ from pingera import ApiClient
66
+ sdk_version = getattr(ApiClient, '__version__', 'Unknown')
67
+ sdk_status = "[green]✓ Installed[/green]"
68
+ except ImportError:
69
+ sdk_version = "Not installed"
70
+ sdk_status = "[red]✗ Not found[/red]"
71
+
72
+ info_content = f"""
73
+ [bold blue]pngr[/bold blue] v{__version__}
74
+ A beautiful CLI tool for Pingera Platform
75
+
76
+ [bold]🌐 Pingera Platform:[/bold]
77
+ • Web Dashboard: [link=https://app.pingera.ru]https://app.pingera.ru[/link]
78
+ • Documentation: [link=https://docs.pingera.ru]https://docs.pingera.ru[/link]
79
+ • Monitor uptime, performance, and availability of your services
80
+
81
+ [bold]📦 Dependencies:[/bold]
82
+ • Pingera SDK: {sdk_status} ({sdk_version})
83
+ • Typer: [green]✓ Installed[/green]
84
+ • Rich: [green]✓ Installed[/green]
85
+
86
+ [bold]🔧 Configuration:[/bold]
87
+ • Config file: {get_config().get('config_path', 'Not found')}
88
+ • API Key: {'[green]✓ Set[/green]' if os.getenv('PINGERA_API_KEY') else '[yellow]⚠ Not set[/yellow]'}
89
+
90
+ [bold]📚 Usage:[/bold]
91
+ Run [cyan]pngr --help[/cyan] to see available commands.
92
+ """
93
+
94
+ panel = Panel(
95
+ info_content.strip(),
96
+ title="ℹ️ System Information",
97
+ border_style="cyan",
98
+ padding=(1, 2),
99
+ )
100
+
101
+ console.print(panel)
102
+
103
+
104
+ @app.command("config")
105
+ def config(
106
+ show: bool = typer.Option(False, "--show", "-s", help="Show current configuration"),
107
+ set_api_key: Optional[str] = typer.Option(None, "--api-key", help="Set Pingera API key"),
108
+ set_output_format: Optional[str] = typer.Option(None, "--output-format", help="Set output format (table, json, yaml)"),
109
+ ):
110
+ """
111
+ Manage pngr configuration
112
+ """
113
+ from .utils.config import set_output_format as save_output_format
114
+
115
+ if set_api_key:
116
+ # In a real implementation, this would save to a config file
117
+ console.print(f"[yellow]Note:[/yellow] Set environment variable PINGERA_API_KEY={set_api_key}")
118
+ console.print("[green]✓[/green] API key configuration noted. Please set as environment variable.")
119
+ return
120
+
121
+ if set_output_format:
122
+ if set_output_format in ['table', 'json', 'yaml']:
123
+ if save_output_format(set_output_format):
124
+ console.print(f"[green]✓[/green] Output format set to: {set_output_format}")
125
+ else:
126
+ console.print(f"[red]✗[/red] Failed to save output format")
127
+ else:
128
+ console.print(f"[red]✗[/red] Invalid output format. Use: table, json, or yaml")
129
+ return
130
+
131
+ if show:
132
+ config_data = get_config()
133
+
134
+ config_content = f"""
135
+ [bold]🔧 Current Configuration:[/bold]
136
+
137
+ [bold]API Settings:[/bold]
138
+ • API Key: {'[green]✓ Set[/green]' if os.getenv('PINGERA_API_KEY') else '[red]✗ Not set[/red]'}
139
+ • Base URL: {config_data.get('base_url', 'https://api.pingera.ru')}
140
+
141
+ [bold]CLI Settings:[/bold]
142
+ • Output Format: {config_data.get('output_format', 'table')}
143
+ • Verbose Mode: {config_data.get('verbose', False)}
144
+ • Color Output: {config_data.get('color', True)}
145
+ """
146
+
147
+ panel = Panel(
148
+ config_content.strip(),
149
+ title="⚙️ Configuration",
150
+ border_style="green",
151
+ padding=(1, 2),
152
+ )
153
+
154
+ console.print(panel)
155
+ else:
156
+ console.print("[yellow]Use --show to display current configuration, --api-key to set API key, or --output-format to set output format[/yellow]")
157
+
158
+
159
+
160
+
161
+
162
+ @app.callback()
163
+ def main(
164
+ version: bool = typer.Option(False, "--version", "-V", help="Show version and exit"),
165
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
166
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table, json, yaml"),
167
+ ):
168
+ """
169
+ 🚀 pngr - a nice CLI for Pingera platform
170
+
171
+ Monitor your services, APIs, and websites with powerful uptime monitoring.
172
+ Manage checks, view results, and get insights right from your terminal.
173
+
174
+ 🌐 Web Dashboard: https://app.pingera.ru
175
+ 📚 Documentation: https://docs.pingera.ru
176
+ 💡 Create an account at https://app.pingera.ru to get started with monitoring and get an API key
177
+
178
+ Built with ❤️ using Typer and Rich for the best CLI experience.
179
+ """
180
+ if version:
181
+ console.print(f"[bold blue]PingeraCLI[/bold blue] v{__version__}")
182
+ raise typer.Exit()
183
+
184
+ if verbose:
185
+ console.print("[dim]Verbose mode enabled[/dim]")
186
+
187
+ # Store current output format in config temporarily for subcommands
188
+ from .utils.config import set_output_format as save_output_format
189
+ save_output_format(output)
190
+
191
+
192
+ def cli_entry_point():
193
+ """
194
+ Entry point for the CLI application
195
+ """
196
+ try:
197
+ app()
198
+ except KeyboardInterrupt:
199
+ console.print("\n[yellow]⚠ Operation cancelled by user[/yellow]")
200
+ sys.exit(1)
201
+ except Exception as e:
202
+ error_console.print(f"[red]Unexpected error:[/red] {str(e)}")
203
+ sys.exit(1)
204
+
205
+
206
+ def main():
207
+ """
208
+ Main function for module execution
209
+ """
210
+ cli_entry_point()
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()
@@ -0,0 +1,8 @@
1
+ """
2
+ Utilities module for PingeraCLI
3
+ """
4
+
5
+ from .console import console, error_console
6
+ from .config import get_config, save_config
7
+
8
+ __all__ = ["console", "error_console", "get_config", "save_config"]