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
|
@@ -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()
|