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,1011 @@
1
+ """
2
+ Checks commands for PingeraCLI
3
+ """
4
+
5
+ import os
6
+ from typing import Optional, List
7
+ from datetime import datetime
8
+
9
+ import typer
10
+ from rich.table import Table
11
+ from rich.panel import Panel
12
+ from rich.prompt import Confirm
13
+
14
+ from .base import BaseCommand
15
+ from ..utils.config import get_api_key
16
+
17
+
18
+ class ChecksCommand(BaseCommand):
19
+ """
20
+ Commands for managing monitoring checks
21
+ """
22
+
23
+ def __init__(self, output_format: Optional[str] = None):
24
+ super().__init__(output_format)
25
+
26
+ def get_client(self):
27
+ """Get Pingera SDK client with authentication"""
28
+ api_key = get_api_key()
29
+ if not api_key:
30
+ self.display_error("API key not found. Use 'pngr auth login --api-key <key>' to set it.")
31
+ raise typer.Exit(1)
32
+
33
+ try:
34
+ from pingera import ApiClient, Configuration
35
+ from pingera.api import ChecksApi
36
+
37
+ # Configure the client
38
+ configuration = Configuration()
39
+ configuration.host = "https://api.pingera.ru"
40
+ configuration.api_key['apiKeyAuth'] = api_key
41
+
42
+ # Create API client
43
+ api_client = ApiClient(configuration)
44
+ return ChecksApi(api_client)
45
+ except ImportError:
46
+ self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
47
+ raise typer.Exit(1)
48
+ except Exception as e:
49
+ self.display_error(f"Failed to initialize client: {str(e)}")
50
+ raise typer.Exit(1)
51
+
52
+ def get_unified_results_client(self):
53
+ """Get Pingera SDK client for unified results with authentication"""
54
+ api_key = get_api_key()
55
+ if not api_key:
56
+ self.display_error("API key not found. Use 'pngr auth login --api-key <key>' to set it.")
57
+ raise typer.Exit(1)
58
+
59
+ try:
60
+ from pingera import ApiClient, Configuration
61
+ from pingera.api import ChecksUnifiedResultsApi
62
+
63
+ # Configure the client
64
+ configuration = Configuration()
65
+ configuration.host = "https://api.pingera.ru"
66
+ configuration.api_key['apiKeyAuth'] = api_key
67
+
68
+ # Create API client
69
+ api_client = ApiClient(configuration)
70
+ return ChecksUnifiedResultsApi(api_client)
71
+ except ImportError:
72
+ self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
73
+ raise typer.Exit(1)
74
+ except Exception as e:
75
+ self.display_error(f"Failed to initialize client: {str(e)}")
76
+ raise typer.Exit(1)
77
+
78
+ def list_checks(self, page: int = 1, page_size: int = 20, check_type: Optional[str] = None, status: Optional[str] = None):
79
+ """List monitoring checks"""
80
+ try:
81
+ checks_api = self.get_client()
82
+
83
+ # Make API call using the actual SDK method
84
+ response = checks_api.v1_checks_get()
85
+
86
+ if not response.checks:
87
+ if self.output_format == 'json':
88
+ self.output_data({"checks": [], "total": 0, "message": "No checks found"})
89
+ elif self.output_format == 'yaml':
90
+ self.output_data({"checks": [], "total": 0, "message": "No checks found"})
91
+ else:
92
+ self.display_info("No checks found.")
93
+ return
94
+
95
+ # Prepare data for different output formats
96
+ if self.output_format in ['json', 'yaml']:
97
+ checks_data = []
98
+ for check in response.checks:
99
+ check_dict = {
100
+ "id": str(check.id) if check.id else None,
101
+ "name": check.name if check.name else None,
102
+ "type": check.type if check.type else None,
103
+ "url": check.url if check.url else None,
104
+ "status": check.status if check.status else None,
105
+ "interval": check.interval if check.interval else None,
106
+ "created_at": check.created_at.isoformat() if hasattr(check, 'created_at') and check.created_at else None
107
+ }
108
+ checks_data.append(check_dict)
109
+
110
+ self.output_data({
111
+ "checks": checks_data,
112
+ "total": len(checks_data),
113
+ "page": page,
114
+ "page_size": page_size
115
+ })
116
+ else:
117
+ # Create table for default output
118
+ table = Table(title=f"Monitoring Checks")
119
+ table.add_column("ID", style="cyan")
120
+ table.add_column("Name", style="green")
121
+ table.add_column("Type", style="blue")
122
+ table.add_column("URL", style="yellow")
123
+ table.add_column("Active", style="magenta")
124
+ table.add_column("Interval", style="white")
125
+ table.add_column("Status", style="white")
126
+ table.add_column("Created", style="dim")
127
+
128
+ for check in response.checks:
129
+ # Convert all values to strings to avoid Rich rendering issues
130
+ check_id = str(check.id) if hasattr(check, 'id') and check.id else "-"
131
+ check_name = str(check.name) if hasattr(check, 'name') and check.name else "-"
132
+ check_type = str(check.type) if hasattr(check, 'type') and check.type else "-"
133
+
134
+ # Handle URL/Host display
135
+ check_url = "-"
136
+ if hasattr(check, 'url') and check.url:
137
+ check_url = str(check.url)
138
+ elif hasattr(check, 'host') and hasattr(check, 'port') and check.host and check.port:
139
+ check_url = f"{check.host}:{check.port}"
140
+
141
+ # Handle active status
142
+ active_status = "✅" if hasattr(check, 'active') and check.active else "❌"
143
+
144
+ # Handle interval
145
+ interval_display = f"{check.interval}s" if hasattr(check, 'interval') and check.interval else "-"
146
+
147
+ # Handle status
148
+ status_display = str(check.status) if hasattr(check, 'status') and check.status else "-"
149
+
150
+ # Handle created_at
151
+ created_display = "-"
152
+ if hasattr(check, 'created_at') and check.created_at:
153
+ try:
154
+ created_display = check.created_at.strftime("%Y-%m-%d %H:%M:%S")
155
+ except (AttributeError, TypeError):
156
+ created_display = str(check.created_at)
157
+
158
+ table.add_row(
159
+ check_id,
160
+ check_name,
161
+ check_type,
162
+ check_url,
163
+ active_status,
164
+ interval_display,
165
+ status_display,
166
+ created_display
167
+ )
168
+
169
+ self.console.print(table)
170
+ self.console.print(f"\n[dim]Found {len(response.checks)} checks[/dim]")
171
+
172
+ except Exception as e:
173
+ self.display_error(f"Failed to list checks: {str(e)}")
174
+ raise typer.Exit(1)
175
+
176
+ def get_check(self, check_id: str):
177
+ """Get specific check details"""
178
+ try:
179
+ checks_api = self.get_client()
180
+ check = checks_api.v1_checks_check_id_get(check_id=check_id)
181
+
182
+ # Prepare data for different output formats
183
+ if self.output_format in ['json', 'yaml']:
184
+ check_data = {
185
+ "id": str(check.id) if check.id else None,
186
+ "name": check.name if check.name else None,
187
+ "type": check.type if check.type else None,
188
+ "status": check.status if check.status else None,
189
+ "active": check.active if hasattr(check, 'active') else None,
190
+ "url": check.url if check.url else None,
191
+ "host": check.host if hasattr(check, 'host') and check.host else None,
192
+ "port": check.port if hasattr(check, 'port') and check.port else None,
193
+ "interval": check.interval if check.interval else None,
194
+ "timeout": check.timeout if check.timeout else None,
195
+ "created_at": check.created_at.isoformat() if hasattr(check, 'created_at') and check.created_at else None,
196
+ "updated_at": check.updated_at.isoformat() if hasattr(check, 'updated_at') and check.updated_at else None,
197
+ "last_checked_at": check.last_checked_at.isoformat() if hasattr(check, 'last_checked_at') and check.last_checked_at else None,
198
+ "parameters": check.parameters if hasattr(check, 'parameters') and check.parameters else None
199
+ }
200
+ self.output_data(check_data)
201
+ else:
202
+ # Rich formatted output for table format
203
+ status_color = "green" if check.status == "ok" else "red" if check.status == "error" else "yellow"
204
+ active_status = "[green]✓ Active[/green]" if check.active else "[red]✗ Inactive[/red]"
205
+
206
+ basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
207
+ • ID: [white]{check.id}[/white]
208
+ • Name: [white]{check.name}[/white]
209
+ • Type: [blue]{check.type}[/blue]
210
+ • Status: [{status_color}]{check.status}[/{status_color}]
211
+ • Active: {active_status}
212
+ • URL: [yellow]{check.url if check.url else 'N/A'}[/yellow]
213
+ • Host: [white]{check.host if hasattr(check, 'host') and check.host else 'N/A'}[/white]
214
+ • Port: [white]{check.port if hasattr(check, 'port') and check.port else 'N/A'}[/white]"""
215
+
216
+ # Timing information
217
+ timing_info = f"""[bold cyan]Timing & Monitoring:[/bold cyan]
218
+ • Interval: [white]{check.interval}s[/white] ({check.interval // 60} minutes)
219
+ • Timeout: [white]{check.timeout}s[/white]
220
+ • Created: [white]{check.created_at.strftime('%Y-%m-%d %H:%M:%S UTC') if check.created_at else 'Unknown'}[/white]
221
+ • Updated: [white]{check.updated_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(check, 'updated_at') and check.updated_at else 'Unknown'}[/white]
222
+ • Last Checked: [white]{check.last_checked_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(check, 'last_checked_at') and check.last_checked_at else 'Never'}[/white]"""
223
+
224
+ # Parameters section
225
+ parameters_info = "[bold cyan]Parameters:[/bold cyan]\n"
226
+ if hasattr(check, 'parameters') and check.parameters:
227
+ params = check.parameters
228
+
229
+ # Handle different check types
230
+ if check.type == 'web' or check.type == 'api':
231
+ if 'http_request' in params:
232
+ http_req = params['http_request']
233
+ parameters_info += f"• Method: [white]{http_req.get('method', 'GET')}[/white]\n"
234
+ if http_req.get('headers'):
235
+ parameters_info += "• Headers:\n"
236
+ for key, value in http_req['headers'].items():
237
+ # Mask sensitive headers
238
+ if key.lower() in ['authorization', 'x-api-key', 'token']:
239
+ value = "***" + value[-4:] if len(value) > 4 else "***"
240
+ parameters_info += f" - {key}: [dim]{value}[/dim]\n"
241
+ if http_req.get('body'):
242
+ # Show more of the body content, only truncate if very long
243
+ if len(http_req['body']) > 500:
244
+ body_preview = http_req['body'][:500] + "..."
245
+ else:
246
+ body_preview = http_req['body']
247
+ parameters_info += f"• Body: [dim]{body_preview}[/dim]\n"
248
+ if http_req.get('username'):
249
+ parameters_info += f"• Username: [white]{http_req['username']}[/white]\n"
250
+
251
+ if 'search_text' in params and params['search_text']:
252
+ parameters_info += f"• Search Text: [white]{params['search_text']}[/white]\n"
253
+
254
+ if 'regions' in params:
255
+ parameters_info += f"• Regions: [white]{', '.join(params['regions'])}[/white]\n"
256
+
257
+ elif check.type == 'multistep' or check.type == 'synthetic':
258
+ if 'pw_script' in params:
259
+ # Display full script without truncation
260
+ parameters_info += f"• Playwright Script:\n[dim]{params['pw_script']}[/dim]\n"
261
+ if 'regions' in params:
262
+ parameters_info += f"• Regions: [white]{', '.join(params['regions'])}[/white]\n"
263
+
264
+ elif check.type == 'ssl':
265
+ if 'assertions' in params:
266
+ assertions = params['assertions']
267
+ if 'expiration_threshold' in assertions:
268
+ days = assertions['expiration_threshold'] // 86400
269
+ parameters_info += f"• Expiration Threshold: [white]{days} days[/white]\n"
270
+
271
+ # Show any other parameters
272
+ for key, value in params.items():
273
+ if key not in ['http_request', 'search_text', 'regions', 'pw_script', 'assertions']:
274
+ parameters_info += f"• {key.replace('_', ' ').title()}: [white]{value}[/white]\n"
275
+ else:
276
+ parameters_info += "• [dim]No parameters configured[/dim]"
277
+
278
+ # Combine all sections
279
+ full_info = f"{basic_info}\n\n{timing_info}\n\n{parameters_info}"
280
+
281
+ panel = Panel(
282
+ full_info,
283
+ title=f"🔍 Check Details: {check.name}",
284
+ border_style="blue",
285
+ padding=(1, 2),
286
+ )
287
+
288
+ self.console.print(panel)
289
+
290
+ except Exception as e:
291
+ self.display_error(f"Failed to get check: {str(e)}")
292
+ raise typer.Exit(1)
293
+
294
+ def create_check(self, name: str, check_type: str, url: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None, interval: int = 300, timeout: int = 30, parameters: Optional[str] = None, pw_script_file: Optional[str] = None):
295
+ """Create a new monitoring check"""
296
+ try:
297
+ import json
298
+ import os
299
+ checks_api = self.get_client()
300
+
301
+ # Build check data
302
+ check_data = {
303
+ "name": name,
304
+ "type": check_type,
305
+ "interval": interval,
306
+ "timeout": timeout
307
+ }
308
+
309
+ # Add URL if provided (required for web, api, ssl checks)
310
+ if url is not None:
311
+ check_data["url"] = url
312
+
313
+ # Add host/port for TCP/SSL checks
314
+ if host is not None:
315
+ check_data["host"] = host
316
+ if port is not None:
317
+ check_data["port"] = port
318
+
319
+ # Handle pw_script_file option
320
+ params_dict = {}
321
+ if pw_script_file is not None:
322
+ if not os.path.exists(pw_script_file):
323
+ self.display_error(f"Playwright script file not found: {pw_script_file}")
324
+ raise typer.Exit(1)
325
+
326
+ try:
327
+ with open(pw_script_file, 'r', encoding='utf-8') as f:
328
+ pw_script_content = f.read().strip()
329
+
330
+ if not pw_script_content:
331
+ self.display_error(f"Playwright script file is empty: {pw_script_file}")
332
+ raise typer.Exit(1)
333
+
334
+ params_dict["pw_script"] = pw_script_content
335
+ self.display_info(f"Loaded Playwright script from: {pw_script_file}")
336
+
337
+ except IOError as e:
338
+ self.display_error(f"Failed to read Playwright script file: {str(e)}")
339
+ raise typer.Exit(1)
340
+
341
+ # Parse parameters JSON if provided
342
+ if parameters is not None:
343
+ try:
344
+ parsed_params = json.loads(parameters)
345
+ # Merge with pw_script from file if both are provided
346
+ params_dict.update(parsed_params)
347
+ except json.JSONDecodeError as e:
348
+ self.display_error(f"Invalid JSON in --parameters: {str(e)}")
349
+ self.display_info("Example: --parameters '{\"pw_script\": \"const { test } = require('@playwright/test'); test('example', async ({ page }) => { await page.goto('https://example.com'); });\", \"regions\": [\"US\", \"EU\"]}'")
350
+ raise typer.Exit(1)
351
+
352
+ # Add parameters to check_data if we have any
353
+ if params_dict:
354
+ check_data["parameters"] = params_dict
355
+
356
+ # Validation based on check type
357
+ if check_type in ['web', 'api'] and not url:
358
+ self.display_error(f"URL is required for {check_type} checks")
359
+ raise typer.Exit(1)
360
+
361
+ if check_type in ['tcp'] and not host:
362
+ self.display_error(f"Host is required for {check_type} checks")
363
+ raise typer.Exit(1)
364
+
365
+ if check_type in ['ssl'] and not (url or host):
366
+ self.display_error(f"Either URL or host is required for {check_type} checks")
367
+ raise typer.Exit(1)
368
+
369
+ if check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
370
+ self.display_error(f"Playwright script is required for {check_type} checks. Use --pw-script-file or --parameters with pw_script")
371
+ raise typer.Exit(1)
372
+
373
+ # Use the actual SDK method
374
+ check = checks_api.v1_checks_post(monitor_check=check_data)
375
+
376
+ # Build success message
377
+ success_details = [f"ID: {check.id}", f"Type: {check_type}"]
378
+ if url:
379
+ success_details.append(f"URL: {url}")
380
+ if host:
381
+ success_details.append(f"Host: {host}")
382
+ if port:
383
+ success_details.append(f"Port: {port}")
384
+ if pw_script_file:
385
+ success_details.append(f"Script: loaded from {pw_script_file}")
386
+
387
+ self.display_success(
388
+ f"Check '{name}' created successfully!\n" + "\n".join(success_details),
389
+ "✅ Check Created"
390
+ )
391
+
392
+ except Exception as e:
393
+ self.display_error(f"Failed to create check: {str(e)}")
394
+ raise typer.Exit(1)
395
+
396
+ def update_check(self, check_id: str, name: Optional[str] = None, url: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None, interval: Optional[int] = None, timeout: Optional[int] = None, active: Optional[bool] = None, parameters: Optional[str] = None, pw_script_file: Optional[str] = None):
397
+ """Update an existing check"""
398
+ try:
399
+ import json
400
+ import os
401
+ checks_api = self.get_client()
402
+
403
+ # Build update data
404
+ update_data = {}
405
+ if name is not None:
406
+ update_data["name"] = name
407
+ if url is not None:
408
+ update_data["url"] = url
409
+ if host is not None:
410
+ update_data["host"] = host
411
+ if port is not None:
412
+ update_data["port"] = port
413
+ if interval is not None:
414
+ update_data["interval"] = interval
415
+ if timeout is not None:
416
+ update_data["timeout"] = timeout
417
+ if active is not None:
418
+ update_data["active"] = active
419
+
420
+ # Handle pw_script_file option
421
+ params_dict = {}
422
+ if pw_script_file is not None:
423
+ if not os.path.exists(pw_script_file):
424
+ self.display_error(f"Playwright script file not found: {pw_script_file}")
425
+ raise typer.Exit(1)
426
+
427
+ try:
428
+ with open(pw_script_file, 'r', encoding='utf-8') as f:
429
+ pw_script_content = f.read().strip()
430
+
431
+ if not pw_script_content:
432
+ self.display_error(f"Playwright script file is empty: {pw_script_file}")
433
+ raise typer.Exit(1)
434
+
435
+ params_dict["pw_script"] = pw_script_content
436
+ self.display_info(f"Loaded Playwright script from: {pw_script_file}")
437
+
438
+ except IOError as e:
439
+ self.display_error(f"Failed to read Playwright script file: {str(e)}")
440
+ raise typer.Exit(1)
441
+
442
+ # Parse parameters JSON if provided
443
+ if parameters is not None:
444
+ try:
445
+ parsed_params = json.loads(parameters)
446
+ # Merge with pw_script from file if both are provided
447
+ params_dict.update(parsed_params)
448
+ except json.JSONDecodeError as e:
449
+ self.display_error(f"Invalid JSON in --parameters: {str(e)}")
450
+ self.display_info("Example: --parameters '{\"pw_script\": \"const { test } = require('@playwright/test'); test('example', async ({ page }) => { await page.goto('https://example.com'); });\", \"regions\": [\"US\", \"EU\"]}'")
451
+ raise typer.Exit(1)
452
+
453
+ # Add parameters to update_data if we have any
454
+ if params_dict:
455
+ update_data["parameters"] = params_dict
456
+
457
+ if not update_data:
458
+ self.display_warning("No updates specified. Use --name, --url, --host, --port, --interval, --timeout, --active/--inactive, --parameters, or --pw-script-file to update.")
459
+ return
460
+
461
+ # Use the actual SDK method with the correct parameter name
462
+ check = checks_api.v1_checks_check_id_patch(check_id=check_id, monitor_check1=update_data)
463
+
464
+ status_msg = ""
465
+ if active is not None:
466
+ status_msg = f"\nStatus: {'Active' if active else 'Paused'}"
467
+
468
+ # Show what was updated
469
+ updated_fields = []
470
+ if name is not None:
471
+ updated_fields.append(f"name: {check.name}")
472
+ if url is not None:
473
+ updated_fields.append(f"url: {check.url}")
474
+ if host is not None:
475
+ updated_fields.append(f"host: {check.host}")
476
+ if port is not None:
477
+ updated_fields.append(f"port: {check.port}")
478
+ if interval is not None:
479
+ updated_fields.append(f"interval: {check.interval}s")
480
+ if timeout is not None:
481
+ updated_fields.append(f"timeout: {check.timeout}s")
482
+ if pw_script_file is not None:
483
+ updated_fields.append(f"pw_script: loaded from {pw_script_file}")
484
+ if parameters is not None:
485
+ updated_fields.append("parameters: updated")
486
+
487
+ update_summary = "\n".join([f"• {field}" for field in updated_fields]) if updated_fields else ""
488
+
489
+ self.display_success(
490
+ f"Check {check_id} updated successfully!\n{update_summary}{status_msg}",
491
+ "✅ Check Updated"
492
+ )
493
+
494
+ except Exception as e:
495
+ self.display_error(f"Failed to update check: {str(e)}")
496
+ raise typer.Exit(1)
497
+
498
+ def delete_check(self, check_id: str, confirm: bool = False):
499
+ """Delete a monitoring check"""
500
+ try:
501
+ if not confirm:
502
+ if not Confirm.ask(f"Are you sure you want to delete check {check_id}?"):
503
+ self.console.print("[yellow]Operation cancelled.[/yellow]")
504
+ return
505
+
506
+ client = self.get_client()
507
+ client.v1_checks_check_id_delete(check_id=check_id)
508
+
509
+ self.display_success(
510
+ f"Check {check_id} deleted successfully!",
511
+ "✅ Check Deleted"
512
+ )
513
+
514
+ except Exception as e:
515
+ self.display_error(f"Failed to delete check: {str(e)}")
516
+ raise typer.Exit(1)
517
+
518
+ def get_check_results(self, check_id: Optional[str] = None, from_date: Optional[str] = None, to_date: Optional[str] = None, page: int = 1, page_size: int = 20, status: Optional[str] = None, check_type: Optional[str] = None, region: Optional[str] = None):
519
+ """Get check results using unified results API"""
520
+ try:
521
+ unified_api = self.get_unified_results_client()
522
+
523
+ # Build parameters for the unified API call
524
+ params = {
525
+ "page": page,
526
+ "page_size": page_size
527
+ }
528
+
529
+ # Add check_id only if provided
530
+ if check_id:
531
+ params["check_id"] = check_id
532
+
533
+ # Add optional filters
534
+ if from_date:
535
+ params["start_date"] = from_date
536
+ if to_date:
537
+ params["end_date"] = to_date
538
+ if status:
539
+ params["status"] = status
540
+ if check_type:
541
+ params["check_type"] = check_type
542
+ if region:
543
+ params["region"] = region
544
+
545
+ # Use the unified results API
546
+ response = unified_api.v1_checks_all_results_get(**params)
547
+
548
+ if not hasattr(response, 'results') or not response.results:
549
+ if self.output_format in ['json', 'yaml']:
550
+ self.output_data({"results": [], "total": 0, "message": "No results found"})
551
+ else:
552
+ self.display_info("No results found.")
553
+ return
554
+
555
+ # Prepare data for different output formats
556
+ if self.output_format in ['json', 'yaml']:
557
+ results_data = []
558
+ for result in response.results:
559
+ result_dict = {
560
+ "id": str(result.id) if hasattr(result, 'id') and result.id else None,
561
+ "check_id": result.check_id if hasattr(result, 'check_id') else None,
562
+ "check_name": result.check_name if hasattr(result, 'check_name') else None,
563
+ "check_type": result.check_type if hasattr(result, 'check_type') else None,
564
+ "status": result.status if hasattr(result, 'status') else None,
565
+ "created_at": result.created_at if hasattr(result, 'created_at') and result.created_at else None,
566
+ "response_time": result.response_time if hasattr(result, 'response_time') else None,
567
+ "error_message": result.error_message if hasattr(result, 'error_message') else None,
568
+ "check_server_id": result.check_server_id if hasattr(result, 'check_server_id') else None,
569
+ "region": result.region if hasattr(result, 'region') else None,
570
+ "result_type": result.result_type if hasattr(result, 'result_type') else None,
571
+ "check_metadata": result.check_metadata if hasattr(result, 'check_metadata') else None,
572
+ "check_server": {
573
+ "ip_address": result.check_server.ip_address if hasattr(result, 'check_server') and result.check_server and hasattr(result.check_server, 'ip_address') else None,
574
+ "country": result.check_server.country if hasattr(result, 'check_server') and result.check_server and hasattr(result.check_server, 'country') else None,
575
+ "region": result.check_server.region if hasattr(result, 'check_server') and result.check_server and hasattr(result.check_server, 'region') else None
576
+ } if hasattr(result, 'check_server') and result.check_server else None
577
+ }
578
+ results_data.append(result_dict)
579
+
580
+ pagination_info = {}
581
+ if hasattr(response, 'pagination') and response.pagination:
582
+ pagination_info = {
583
+ "page": response.pagination.get('page', page),
584
+ "page_size": response.pagination.get('page_size', page_size),
585
+ "total_items": response.pagination.get('total_items', len(results_data)),
586
+ "total_pages": response.pagination.get('total_pages', 1)
587
+ }
588
+
589
+ self.output_data({
590
+ "results": results_data,
591
+ "pagination": pagination_info if pagination_info else {"page": page, "page_size": page_size, "total_items": len(results_data)}
592
+ })
593
+
594
+ else:
595
+ # Create table for human-readable output
596
+ table_title = f"Check Results for {check_id}" if check_id else "All Check Results"
597
+ table = Table(title=table_title)
598
+ table.add_column("Result ID", style="dim", min_width=15)
599
+ table.add_column("Check/Job ID", style="cyan", min_width=15)
600
+ table.add_column("Timestamp", style="cyan")
601
+ table.add_column("Check Name", style="green", max_width=20)
602
+ table.add_column("Type", style="blue")
603
+ table.add_column("Status", style="green")
604
+ table.add_column("Response Time", style="yellow")
605
+ table.add_column("Region", style="magenta", max_width=12)
606
+ table.add_column("Error", style="red", max_width=25)
607
+
608
+ for result in response.results:
609
+ # Extract result ID - never truncate IDs
610
+ result_id = str(result.id) if hasattr(result, 'id') and result.id else "-"
611
+
612
+ # Extract check_id or job_id based on result type - never truncate IDs
613
+ check_or_job_id = "-"
614
+ if hasattr(result, 'check_id') and result.check_id:
615
+ check_or_job_id = str(result.check_id)
616
+ elif hasattr(result, 'result_type') and result.result_type == 'on_demand':
617
+ # For on-demand results, we might not have check_id, show as on-demand
618
+ check_or_job_id = "[dim]on-demand[/dim]"
619
+
620
+ # Extract timestamp
621
+ timestamp = result.created_at.strftime("%m-%d %H:%M:%S") if hasattr(result, 'created_at') and result.created_at else "-"
622
+
623
+ # Check name
624
+ check_name = result.check_name if hasattr(result, 'check_name') and result.check_name else "-"
625
+ if len(check_name) > 19:
626
+ check_name = check_name[:19] + "…"
627
+
628
+ # Check type
629
+ check_type_display = result.check_type if hasattr(result, 'check_type') and result.check_type else "-"
630
+
631
+ # Determine status with emoji
632
+ status_emoji = "✅" if hasattr(result, 'status') and result.status == 'ok' else "❌"
633
+ status_color = "green" if hasattr(result, 'status') and result.status == 'ok' else "red"
634
+ status_display = f"[{status_color}]{status_emoji} {result.status}[/{status_color}]" if hasattr(result, 'status') and result.status else "-"
635
+
636
+ # Extract response time
637
+ response_time = f"{result.response_time}ms" if hasattr(result, 'response_time') and result.response_time is not None else "-"
638
+
639
+ # Extract region
640
+ region_display = result.region if hasattr(result, 'region') and result.region else "-"
641
+ if len(region_display) > 11:
642
+ region_display = region_display[:11] + "…"
643
+
644
+ # Extract error message
645
+ error_message = "-"
646
+ if hasattr(result, 'error_message') and result.error_message:
647
+ error_message = result.error_message
648
+ # Truncate long error messages
649
+ if len(error_message) > 24:
650
+ error_message = error_message[:24] + "…"
651
+
652
+ table.add_row(
653
+ result_id,
654
+ check_or_job_id,
655
+ timestamp,
656
+ check_name,
657
+ check_type_display,
658
+ status_display,
659
+ response_time,
660
+ region_display,
661
+ error_message
662
+ )
663
+
664
+ self.console.print(table)
665
+
666
+ # Show pagination info and hint about detailed view
667
+ if hasattr(response, 'pagination') and response.pagination:
668
+ pagination = response.pagination
669
+ total_items = pagination.get('total_items', len(response.results))
670
+ total_pages = pagination.get('total_pages', 1)
671
+ current_page = pagination.get('page', page)
672
+ page_size_actual = pagination.get('page_size', page_size)
673
+
674
+ self.console.print(f"\n[dim]Showing {len(response.results)} results • Page {current_page} of {total_pages} • {total_items} total items • {page_size_actual} per page[/dim]")
675
+ else:
676
+ self.console.print(f"\n[dim]Found {len(response.results)} results[/dim]")
677
+
678
+ # Provide helpful hints based on context
679
+ hints = []
680
+ if check_id:
681
+ hints.append(f"Use 'pngr checks result {check_id} <result-id>' for detailed result info")
682
+ else:
683
+ hints.append("Use 'pngr checks result <check-id> <result-id>' for detailed result info")
684
+ hints.append("For on-demand checks: use 'pngr checks jobs result <result-id>' for detailed info")
685
+ hints.append("Use 'pngr checks list' to see all available checks and their IDs")
686
+ hints.append("Filter results: --type <type>, --status <status>, --region <region>")
687
+
688
+ for hint in hints:
689
+ self.console.print(f"[dim]💡 {hint}[/dim]")
690
+
691
+ except Exception as e:
692
+ self.display_error(f"Failed to get check results: {str(e)}")
693
+ raise typer.Exit(1)
694
+
695
+ def get_check_result(self, result_id: str):
696
+ """Get detailed information for a specific check result"""
697
+ try:
698
+ checks_api = self.get_client()
699
+
700
+ # Parse the result ID to extract check_id and check_result_id
701
+ # The result_id format might be "check_id:result_id" or just "result_id"
702
+ # We need to determine the check_id somehow - for now, we'll ask user to provide it
703
+
704
+ # Since we need both check_id and check_result_id, we'll need to modify the command
705
+ # For now, let's show an error and guide the user
706
+ self.display_error("This command needs both check ID and result ID.")
707
+ self.display_info("Usage: pngr checks result <check-id> <result-id>")
708
+ self.display_info("Or use: pngr checks results <check-id> to list all results first")
709
+ return
710
+
711
+ except Exception as e:
712
+ self.display_error(f"Failed to get result details: {str(e)}")
713
+ raise typer.Exit(1)
714
+
715
+ def get_check_result_detailed(self, check_id: str, result_id: str, verbose: bool = False):
716
+ """Get detailed information for a specific check result using check_id and result_id"""
717
+ try:
718
+ checks_api = self.get_client()
719
+
720
+ # Use the actual SDK method
721
+ result = checks_api.v1_checks_check_id_results_check_result_id_get(
722
+ check_id=check_id,
723
+ check_result_id=result_id
724
+ )
725
+
726
+ if self.output_format in ['json', 'yaml']:
727
+ # Full result data for JSON/YAML
728
+ result_data = {
729
+ "id": str(result.id) if hasattr(result, 'id') else None,
730
+ "check_id": result.check_id if hasattr(result, 'check_id') else None,
731
+ "status": result.status if hasattr(result, 'status') else None,
732
+ "created_at": result.created_at if hasattr(result, 'created_at') and result.created_at else None,
733
+ "response_time": result.response_time if hasattr(result, 'response_time') else None,
734
+ "error_message": result.error_message if hasattr(result, 'error_message') else None,
735
+ "check_server_id": result.check_server_id if hasattr(result, 'check_server_id') else None,
736
+ "check_metadata": result.check_metadata if hasattr(result, 'check_metadata') else None
737
+ }
738
+ self.output_data(result_data)
739
+ else:
740
+ # Rich formatted detailed view
741
+ self._display_detailed_result(result, verbose)
742
+
743
+ except Exception as e:
744
+ self.display_error(f"Failed to get result details: {str(e)}")
745
+ raise typer.Exit(1)
746
+
747
+ def _display_detailed_result(self, result, verbose: bool = False):
748
+ """Display detailed result information in a rich format"""
749
+
750
+ # Basic information
751
+ response_time_display = f"{result.response_time}ms" if hasattr(result, 'response_time') and result.response_time else 'N/A'
752
+ basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
753
+ • Result ID: [white]{result.id}[/white]
754
+ • Check ID: [white]{result.check_id}[/white]
755
+ • Status: [{"green" if result.status == "ok" else "red"}]{result.status}[/{"green" if result.status == "ok" else "red"}]
756
+ • Timestamp: [white]{result.created_at.strftime('%Y-%m-%d %H:%M:%S UTC') if result.created_at else 'Unknown'}[/white]
757
+ • Response Time: [yellow]{response_time_display}[/yellow]
758
+ • Check Server: [magenta]{result.check_server_id if result.check_server_id else 'Unknown'}[/magenta]"""
759
+
760
+ # Add check server details if available
761
+ if hasattr(result, 'check_server') and result.check_server:
762
+ server = result.check_server
763
+ basic_info += f"""
764
+ • Server IP: [white]{server.ip_address if hasattr(server, 'ip_address') else 'Unknown'}[/white]
765
+ • Server Country: [white]{server.country if hasattr(server, 'country') else 'Unknown'}[/white]
766
+ • Server Region: [white]{server.region if hasattr(server, 'region') else 'Unknown'}[/white]"""
767
+
768
+ # Error information
769
+ error_info = ""
770
+ if hasattr(result, 'error_message') and result.error_message:
771
+ error_info = f"""
772
+ [bold red]Error Information:[/bold red]
773
+ • Error: [red]{result.error_message}[/red]"""
774
+
775
+ # Metadata analysis - handle different check types
776
+ metadata_info = ""
777
+ if hasattr(result, 'check_metadata') and result.check_metadata:
778
+ metadata = result.check_metadata
779
+
780
+ if isinstance(metadata, dict):
781
+ metadata_info = self._format_metadata_by_type(metadata, verbose)
782
+
783
+ # Combine all sections
784
+ full_info = basic_info
785
+ if error_info:
786
+ full_info += error_info
787
+ if metadata_info:
788
+ full_info += metadata_info
789
+
790
+ panel = Panel(
791
+ full_info,
792
+ title=f"🔍 Result Details: {result.id}",
793
+ border_style="blue",
794
+ padding=(1, 2),
795
+ )
796
+
797
+ self.console.print(panel)
798
+
799
+ def _format_metadata_by_type(self, metadata, verbose: bool = False):
800
+ """Format metadata using the appropriate formatter"""
801
+ from ..formatters.registry import FormatterRegistry
802
+ registry = FormatterRegistry(verbose)
803
+ return registry.format_metadata(metadata)
804
+
805
+ def list_regions(self, check_type: Optional[str] = None):
806
+ """List available regions for checks"""
807
+ try:
808
+ checks_api = self.get_client()
809
+
810
+ # Get regions with optional check_type filter
811
+ if check_type:
812
+ response = checks_api.v1_checks_get_regions_get(check_type=check_type)
813
+ else:
814
+ response = checks_api.v1_checks_get_regions_get()
815
+
816
+ if not hasattr(response, 'regions') or not response.regions:
817
+ if self.output_format in ['json', 'yaml']:
818
+ self.output_data({"regions": [], "total": 0, "message": "No regions found"})
819
+ else:
820
+ self.display_info("No regions found.")
821
+ return
822
+
823
+ # Prepare data for different output formats
824
+ if self.output_format in ['json', 'yaml']:
825
+ regions_data = []
826
+ for region in response.regions:
827
+ region_dict = {
828
+ "id": str(region.id) if hasattr(region, 'id') and region.id else None,
829
+ "display_name": region.display_name if hasattr(region, 'display_name') and region.display_name else None,
830
+ "available_check_types": region.available_check_types if hasattr(region, 'available_check_types') and region.available_check_types else None
831
+ }
832
+ regions_data.append(region_dict)
833
+
834
+ self.output_data({
835
+ "regions": regions_data,
836
+ "total": len(regions_data),
837
+ "filter": {"check_type": check_type} if check_type else None
838
+ })
839
+ else:
840
+ # Create table for default output
841
+ table_title = f"Available Regions for {check_type.upper()} checks" if check_type else "Available Regions"
842
+ table = Table(title=table_title)
843
+ table.add_column("Region ID", style="cyan", min_width=15)
844
+ table.add_column("Display Name", style="green", min_width=25)
845
+ table.add_column("Available Check Types", style="white")
846
+
847
+ for region in response.regions:
848
+ # Extract region data using actual SDK attributes
849
+ region_id = str(region.id) if hasattr(region, 'id') and region.id else "-"
850
+ display_name = region.display_name if hasattr(region, 'display_name') and region.display_name else "-"
851
+
852
+ # Handle available check types
853
+ available_types = "-"
854
+ if hasattr(region, 'available_check_types') and region.available_check_types:
855
+ if isinstance(region.available_check_types, list):
856
+ available_types = ", ".join(region.available_check_types)
857
+ else:
858
+ available_types = str(region.available_check_types)
859
+
860
+ table.add_row(
861
+ region_id,
862
+ display_name,
863
+ available_types
864
+ )
865
+
866
+ self.console.print(table)
867
+
868
+ # Show summary
869
+ filter_text = f" for {check_type} checks" if check_type else ""
870
+ self.console.print(f"\n[dim]Found {len(response.regions)} regions{filter_text}[/dim]")
871
+
872
+ # Show helpful hints
873
+ if not check_type:
874
+ self.console.print(f"[dim]💡 Filter by check type: pngr checks list-regions --type <type>[/dim]")
875
+ self.console.print(f"[dim]💡 Available check types: web, api, tcp, ssl, synthetic, multistep[/dim]")
876
+
877
+ except Exception as e:
878
+ self.display_error(f"Failed to list regions: {str(e)}")
879
+ raise typer.Exit(1)
880
+
881
+ # Create Typer app for checks commands
882
+ app = typer.Typer(name="checks", help="🔍 Manage monitoring checks")
883
+
884
+
885
+ def get_output_format():
886
+ """Get output format from config"""
887
+ from ..utils.config import get_config
888
+ return get_config().get('output_format', 'table')
889
+
890
+
891
+ @app.command("list")
892
+ def list_checks(
893
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
894
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page (max 100)"),
895
+ check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by check type"),
896
+ status: Optional[str] = typer.Option(None, "--status", help="Filter by status"),
897
+ ):
898
+ """List monitoring checks"""
899
+ checks_cmd = ChecksCommand(get_output_format())
900
+ checks_cmd.list_checks(page, page_size, check_type, status)
901
+
902
+
903
+ @app.command("get")
904
+ def get_check(
905
+ check_id: str = typer.Argument(..., help="Check ID to retrieve"),
906
+ ):
907
+ """Get specific check details"""
908
+ checks_cmd = ChecksCommand(get_output_format())
909
+ checks_cmd.get_check(check_id)
910
+
911
+
912
+ @app.command("create")
913
+ def create_check(
914
+ name: str = typer.Option(..., "--name", "-n", help="Check name"),
915
+ check_type: str = typer.Option(..., "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
916
+ url: Optional[str] = typer.Option(None, "--url", "-u", help="URL to monitor (required for web, api, ssl checks)"),
917
+ host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL checks (max 255 characters)"),
918
+ port: Optional[int] = typer.Option(None, "--port", help="Port number for TCP checks (1-65535)"),
919
+ interval: int = typer.Option(300, "--interval", "-i", help="Check interval in seconds"),
920
+ timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
921
+ parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
922
+ pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks")
923
+ ):
924
+ """Create a new monitoring check. Parameters vary by check type:
925
+ - web/api: --url required
926
+ - tcp: --host required, --port optional
927
+ - ssl: --url or --host required
928
+ - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
929
+ checks_cmd = ChecksCommand(get_output_format())
930
+ checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file)
931
+
932
+
933
+ @app.command("update")
934
+ def update_check(
935
+ check_id: str = typer.Argument(..., help="Check ID to update"),
936
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="New check name"),
937
+ url: Optional[str] = typer.Option(None, "--url", "-u", help="New URL"),
938
+ host: Optional[str] = typer.Option(None, "--host", help="New hostname/IP for TCP/SSL checks (max 255 characters)"),
939
+ port: Optional[int] = typer.Option(None, "--port", help="New port number for TCP checks (1-65535)"),
940
+ interval: Optional[int] = typer.Option(None, "--interval", "-i", help="New interval in seconds"),
941
+ timeout: Optional[int] = typer.Option(None, "--timeout", help="New timeout in seconds"),
942
+ active: bool = typer.Option(None, "--active/--inactive", help="Enable or disable the check"),
943
+ parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"pw_script\": \"...\", \"regions\": [\"US\", \"EU\"]}')"),
944
+ pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks")
945
+ ):
946
+ """Update an existing check. Use --parameters to provide complex parameters like Playwright scripts, regions, etc."""
947
+ checks_cmd = ChecksCommand(get_output_format())
948
+ checks_cmd.update_check(check_id, name, url, host, port, interval, timeout, active, parameters, pw_script_file)
949
+
950
+
951
+ @app.command("delete")
952
+ def delete_check(
953
+ check_id: str = typer.Argument(..., help="Check ID to delete"),
954
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"),
955
+ ):
956
+ """Delete a monitoring check"""
957
+ checks_cmd = ChecksCommand(get_output_format())
958
+ checks_cmd.delete_check(check_id, confirm)
959
+
960
+
961
+ @app.command("results")
962
+ def get_results(
963
+ check_id: Optional[str] = typer.Argument(None, help="Check ID (optional - if not provided, returns all results)"),
964
+ from_date: Optional[str] = typer.Option(None, "--from", help="Start date (ISO 8601) - max 6 months ago"),
965
+ to_date: Optional[str] = typer.Option(None, "--to", help="End date (ISO 8601)"),
966
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
967
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page (1-100)"),
968
+ status: Optional[str] = typer.Option(None, "--status", help="Filter by status (ok, failed, degraded, timeout, pending)"),
969
+ check_type: Optional[str] = typer.Option(None, "--type", help="Filter by check type (web, api, tcp, ssl, synthetic, multistep)"),
970
+ region: Optional[str] = typer.Option(None, "--region", help="Filter by region"),
971
+ ):
972
+ """Get check results with advanced filtering. If no check_id is provided, returns unified results across all checks."""
973
+ checks_cmd = ChecksCommand(get_output_format())
974
+ checks_cmd.get_check_results(check_id, from_date, to_date, page, page_size, status, check_type, region)
975
+
976
+
977
+ @app.command("result")
978
+ def get_result(
979
+ check_id: str = typer.Argument(..., help="Check ID"),
980
+ result_id: str = typer.Argument(..., help="Result ID to retrieve detailed information for"),
981
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full detailed information including network requests"),
982
+ ):
983
+ """Get detailed information for a specific check result"""
984
+ checks_cmd = ChecksCommand(get_output_format())
985
+ checks_cmd.get_check_result_detailed(check_id, result_id, verbose)
986
+
987
+
988
+ @app.command("list-regions")
989
+ def list_regions(
990
+ check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter regions by check type (web, api, tcp, ssl, synthetic, multistep)"),
991
+ ):
992
+ """List available regions for monitoring checks"""
993
+ checks_cmd = ChecksCommand(get_output_format())
994
+ checks_cmd.list_regions(check_type)
995
+
996
+
997
+
998
+
999
+
1000
+
1001
+
1002
+
1003
+
1004
+
1005
+
1006
+ # Import on-demand checks functionality from separate module
1007
+ from .on_demand_checks import run_app, jobs_app
1008
+
1009
+ # Add the run and jobs subcommands to the main checks app
1010
+ app.add_typer(run_app, name="run")
1011
+ app.add_typer(jobs_app, name="jobs")