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,646 @@
1
+
2
+ """
3
+ On-demand checks commands for PingeraCLI
4
+ """
5
+
6
+ import os
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.table import Table
11
+ from rich.panel import Panel
12
+
13
+ from .base import BaseCommand
14
+ from ..utils.config import get_api_key
15
+
16
+
17
+ class OnDemandChecksCommand(BaseCommand):
18
+ """
19
+ Commands for managing on-demand monitoring checks
20
+ """
21
+
22
+ def __init__(self, output_format: Optional[str] = None):
23
+ super().__init__(output_format)
24
+
25
+ def get_client(self):
26
+ """Get Pingera SDK client with authentication for on-demand checks"""
27
+ api_key = get_api_key()
28
+ if not api_key:
29
+ self.display_error("API key not found. Use 'pngr auth login --api-key <key>' to set it.")
30
+ raise typer.Exit(1)
31
+
32
+ try:
33
+ from pingera import ApiClient, Configuration
34
+ from pingera.api import OnDemandChecksApi
35
+
36
+ # Configure the client
37
+ configuration = Configuration()
38
+ configuration.host = "https://api.pingera.ru"
39
+ configuration.api_key['apiKeyAuth'] = api_key
40
+
41
+ # Create API client
42
+ api_client = ApiClient(configuration)
43
+ return OnDemandChecksApi(api_client)
44
+ except ImportError:
45
+ self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
46
+ raise typer.Exit(1)
47
+ except Exception as e:
48
+ self.display_error(f"Failed to initialize client: {str(e)}")
49
+ raise typer.Exit(1)
50
+
51
+ def execute_custom_check(self, url: Optional[str] = None, check_type: str = "web", host: Optional[str] = None, port: Optional[int] = None, timeout: int = 30, name: str = "On-demand check", parameters: Optional[str] = None, pw_script_file: Optional[str] = None, wait_for_result: bool = False):
52
+ """Execute custom on-demand check"""
53
+ try:
54
+ import json
55
+ import os
56
+ checks_api = self.get_client()
57
+
58
+ # Import the SDK models
59
+ from pingera.models import ExecuteCustomCheckRequest
60
+
61
+ # Build check data
62
+ check_data = {
63
+ "name": name,
64
+ "type": check_type,
65
+ "timeout": timeout
66
+ }
67
+
68
+ # Add URL if provided (required for web, api, ssl checks)
69
+ if url is not None:
70
+ check_data["url"] = url
71
+
72
+ # Add host/port for TCP/SSL checks
73
+ if host is not None:
74
+ check_data["host"] = host
75
+ if port is not None:
76
+ check_data["port"] = port
77
+
78
+ # Handle pw_script_file option
79
+ params_dict = {}
80
+ if pw_script_file is not None:
81
+ if not os.path.exists(pw_script_file):
82
+ self.display_error(f"Playwright script file not found: {pw_script_file}")
83
+ raise typer.Exit(1)
84
+
85
+ try:
86
+ with open(pw_script_file, 'r', encoding='utf-8') as f:
87
+ pw_script_content = f.read().strip()
88
+
89
+ if not pw_script_content:
90
+ self.display_error(f"Playwright script file is empty: {pw_script_file}")
91
+ raise typer.Exit(1)
92
+
93
+ params_dict["pw_script"] = pw_script_content
94
+ self.display_info(f"Loaded Playwright script from: {pw_script_file}")
95
+
96
+ except IOError as e:
97
+ self.display_error(f"Failed to read Playwright script file: {str(e)}")
98
+ raise typer.Exit(1)
99
+
100
+ # Parse parameters JSON if provided
101
+ if parameters is not None:
102
+ try:
103
+ parsed_params = json.loads(parameters)
104
+ # Merge with pw_script from file if both are provided
105
+ params_dict.update(parsed_params)
106
+ except json.JSONDecodeError as e:
107
+ self.display_error(f"Invalid JSON in --parameters: {str(e)}")
108
+ 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\"]}'")
109
+ raise typer.Exit(1)
110
+
111
+ # Add parameters to check_data if we have any
112
+ if params_dict:
113
+ check_data["parameters"] = params_dict
114
+
115
+ # Validation based on check type
116
+ if check_type in ['web', 'api'] and not url:
117
+ self.display_error(f"URL is required for {check_type} checks")
118
+ raise typer.Exit(1)
119
+
120
+ if check_type in ['tcp'] and not host:
121
+ self.display_error(f"Host is required for {check_type} checks")
122
+ raise typer.Exit(1)
123
+
124
+ if check_type in ['ssl'] and not (url or host):
125
+ self.display_error(f"Either URL or host is required for {check_type} checks")
126
+ raise typer.Exit(1)
127
+
128
+ if check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
129
+ self.display_error(f"Playwright script is required for {check_type} checks. Use --pw-script-file or --parameters with pw_script")
130
+ raise typer.Exit(1)
131
+
132
+ # Create the request
133
+ check_request = ExecuteCustomCheckRequest(**check_data)
134
+
135
+ # Execute the check
136
+ response = checks_api.v1_checks_execute_post(check_request)
137
+ job_id = response.job_id
138
+
139
+ # Build success message
140
+ success_details = [f"Job ID: {job_id}", f"Type: {check_type}"]
141
+ if url:
142
+ success_details.append(f"URL: {url}")
143
+ if host:
144
+ success_details.append(f"Host: {host}")
145
+ if port:
146
+ success_details.append(f"Port: {port}")
147
+ if pw_script_file:
148
+ success_details.append(f"Script: loaded from {pw_script_file}")
149
+
150
+ if wait_for_result:
151
+ # Wait for the job to complete and show the result
152
+ self._wait_and_show_result(job_id, success_details)
153
+ else:
154
+ if self.output_format in ['json', 'yaml']:
155
+ self.output_data({
156
+ "job_id": job_id,
157
+ "check_type": check_type,
158
+ "url": url,
159
+ "host": host,
160
+ "port": port,
161
+ "name": name,
162
+ "status": "queued"
163
+ })
164
+ else:
165
+ self.display_success(
166
+ f"On-demand check queued successfully!\n" + "\n".join(success_details) + f"\n\nUse 'pngr checks jobs result {job_id}' to check status.",
167
+ "✅ Check Queued"
168
+ )
169
+
170
+ except Exception as e:
171
+ self.display_error(f"Failed to execute custom check: {str(e)}")
172
+ raise typer.Exit(1)
173
+
174
+ def execute_existing_check(self, check_id: str, wait_for_result: bool = False):
175
+ """Execute existing check on demand"""
176
+ try:
177
+ checks_api = self.get_client()
178
+
179
+ # Execute existing check
180
+ response = checks_api.v1_checks_check_id_execute_post(check_id=check_id)
181
+ job_id = response.job_id
182
+
183
+ if wait_for_result:
184
+ # Wait for the job to complete and show the result
185
+ success_details = [f"Job ID: {job_id}", f"Check ID: {check_id}"]
186
+ self._wait_and_show_result(job_id, success_details)
187
+ else:
188
+ if self.output_format in ['json', 'yaml']:
189
+ self.output_data({
190
+ "job_id": job_id,
191
+ "check_id": check_id,
192
+ "status": "queued"
193
+ })
194
+ else:
195
+ self.display_success(
196
+ f"Existing check executed successfully!\nJob ID: {job_id}\nCheck ID: {check_id}\n\nUse 'pngr checks jobs result {job_id}' to check status.",
197
+ "✅ Check Executed"
198
+ )
199
+
200
+ except Exception as e:
201
+ self.display_error(f"Failed to execute existing check: {str(e)}")
202
+ raise typer.Exit(1)
203
+
204
+
205
+ def list_jobs(self, page: int = 1, page_size: int = 20):
206
+ """List check jobs"""
207
+ try:
208
+ checks_api = self.get_client()
209
+
210
+ # List check jobs
211
+ response = checks_api.v1_checks_jobs_get(
212
+ page=page,
213
+ per_page=page_size
214
+ )
215
+
216
+ if not hasattr(response, 'jobs') or not response.jobs:
217
+ if self.output_format in ['json', 'yaml']:
218
+ self.output_data({"jobs": [], "total": 0, "message": "No jobs found"})
219
+ else:
220
+ self.display_info("No jobs found.")
221
+ return
222
+
223
+ if self.output_format in ['json', 'yaml']:
224
+ jobs_data = []
225
+ for job in response.jobs:
226
+ # Extract name and type from check_parameters
227
+ name = None
228
+ check_type = None
229
+ host = None
230
+ url = None
231
+
232
+ if hasattr(job, 'check_parameters') and job.check_parameters:
233
+ params = job.check_parameters
234
+ name = params.get('name')
235
+ check_type = params.get('type')
236
+ host = params.get('host')
237
+ # For web checks, construct URL from host/url
238
+ if check_type == 'web' and 'url' in params:
239
+ url = params['url']
240
+ elif host:
241
+ url = host
242
+
243
+ job_dict = {
244
+ "job_id": str(job.id) if job.id else None,
245
+ "name": name,
246
+ "type": check_type,
247
+ "url": url,
248
+ "host": host,
249
+ "status": job.status if hasattr(job, 'status') else None,
250
+ "job_type": job.job_type if hasattr(job, 'job_type') else None,
251
+ "check_id": job.check_id if hasattr(job, 'check_id') else None,
252
+ "created_at": job.created_at.isoformat() if hasattr(job, 'created_at') and job.created_at else None,
253
+ "started_at": job.started_at.isoformat() if hasattr(job, 'started_at') and job.started_at else None,
254
+ "completed_at": job.completed_at.isoformat() if hasattr(job, 'completed_at') and job.completed_at else None,
255
+ "error_message": job.error_message if hasattr(job, 'error_message') else None
256
+ }
257
+ jobs_data.append(job_dict)
258
+
259
+ # Include pagination info from response
260
+ pagination_info = {}
261
+ if hasattr(response, 'pagination') and response.pagination:
262
+ pagination_info = {
263
+ "page": response.pagination.get('page', page),
264
+ "per_page": response.pagination.get('per_page', page_size),
265
+ "total": response.pagination.get('total', len(jobs_data)),
266
+ "pages": response.pagination.get('pages', 1),
267
+ "has_next": response.pagination.get('has_next', False),
268
+ "has_prev": response.pagination.get('has_prev', False)
269
+ }
270
+
271
+ self.output_data({
272
+ "jobs": jobs_data,
273
+ "pagination": pagination_info if pagination_info else {"page": page, "per_page": page_size, "total": len(jobs_data)}
274
+ })
275
+ else:
276
+ table = Table(title="Check Jobs")
277
+ table.add_column("Job ID", style="cyan")
278
+ table.add_column("Name", style="green")
279
+ table.add_column("Type", style="blue")
280
+ table.add_column("Target", style="yellow")
281
+ table.add_column("Status", style="magenta")
282
+ table.add_column("Job Type", style="dim")
283
+ table.add_column("Created", style="dim")
284
+
285
+ for job in response.jobs:
286
+ # Extract details from check_parameters
287
+ name = "Unknown"
288
+ check_type = "Unknown"
289
+ target = "Unknown"
290
+
291
+ if hasattr(job, 'check_parameters') and job.check_parameters:
292
+ params = job.check_parameters
293
+ name = params.get('name', 'Unknown')
294
+ check_type = params.get('type', 'Unknown')
295
+
296
+ # Determine target based on check type
297
+ if check_type == 'web' and 'url' in params:
298
+ target = params['url']
299
+ elif 'host' in params:
300
+ port = params.get('port')
301
+ target = f"{params['host']}" + (f":{port}" if port else "")
302
+ else:
303
+ target = params.get('url', params.get('host', 'Unknown'))
304
+
305
+ # Status with color coding
306
+ status_color = "green" if hasattr(job, 'status') and job.status == 'completed' else "yellow" if hasattr(job, 'status') and job.status in ['running', 'pending'] else "red"
307
+ status_display = f"[{status_color}]{job.status}[/{status_color}]" if hasattr(job, 'status') and job.status else "-"
308
+
309
+ table.add_row(
310
+ str(job.id) if job.id else "-",
311
+ name,
312
+ check_type,
313
+ target,
314
+ status_display,
315
+ job.job_type if hasattr(job, 'job_type') and job.job_type else "-",
316
+ job.created_at.strftime("%Y-%m-%d %H:%M") if hasattr(job, 'created_at') and job.created_at else "Unknown"
317
+ )
318
+
319
+ self.console.print(table)
320
+
321
+ # Show pagination info
322
+ if hasattr(response, 'pagination') and response.pagination:
323
+ pagination = response.pagination
324
+ total_items = pagination.get('total', len(response.jobs))
325
+ total_pages = pagination.get('pages', 1)
326
+ current_page = pagination.get('page', page)
327
+ per_page = pagination.get('per_page', page_size)
328
+
329
+ self.console.print(f"\n[dim]Showing {len(response.jobs)} jobs • Page {current_page} of {total_pages} • {total_items} total items • {per_page} per page[/dim]")
330
+ else:
331
+ self.console.print(f"\n[dim]Found {len(response.jobs)} jobs[/dim]")
332
+
333
+ # Add helpful tip about getting job details
334
+ self.console.print(f"\n[dim]💡 For detailed job information, use: [white]pngr checks jobs status <job_id>[/white][/dim]")
335
+
336
+ except Exception as e:
337
+ self.display_error(f"Failed to list jobs: {str(e)}")
338
+ raise typer.Exit(1)
339
+
340
+ def get_job_status(self, job_id: str):
341
+ """Get job status"""
342
+ try:
343
+ checks_api = self.get_client()
344
+
345
+ # Get job status
346
+ job_status = checks_api.v1_checks_jobs_job_id_get(job_id=job_id)
347
+
348
+ if self.output_format in ['json', 'yaml']:
349
+ # Include full job data for JSON/YAML output
350
+ job_data = {
351
+ "job_id": job_id,
352
+ "status": job_status.status if hasattr(job_status, 'status') else None,
353
+ "job_type": job_status.job_type if hasattr(job_status, 'job_type') else None,
354
+ "check_id": job_status.check_id if hasattr(job_status, 'check_id') else None,
355
+ "created_at": job_status.created_at.isoformat() if hasattr(job_status, 'created_at') and job_status.created_at else None,
356
+ "started_at": job_status.started_at.isoformat() if hasattr(job_status, 'started_at') and job_status.started_at else None,
357
+ "completed_at": job_status.completed_at.isoformat() if hasattr(job_status, 'completed_at') and job_status.completed_at else None,
358
+ "error_message": job_status.error_message if hasattr(job_status, 'error_message') else None,
359
+ "check_parameters": job_status.check_parameters if hasattr(job_status, 'check_parameters') else None,
360
+ "result": job_status.result if hasattr(job_status, 'result') else None
361
+ }
362
+ self.output_data(job_data)
363
+ else:
364
+ # Rich formatted detailed view
365
+ self._display_detailed_job_status(job_status, job_id)
366
+
367
+ except Exception as e:
368
+ self.display_error(f"Failed to get job status: {str(e)}")
369
+ raise typer.Exit(1)
370
+
371
+ def _display_detailed_job_status(self, job_status, job_id: str):
372
+ """Display detailed job status information in a rich format"""
373
+
374
+ # Determine status color and emoji
375
+ status_emoji = "✅" if hasattr(job_status, 'status') and job_status.status == 'completed' else "🏃" if hasattr(job_status, 'status') and job_status.status in ['running', 'pending'] else "❌"
376
+ status_color = "green" if hasattr(job_status, 'status') and job_status.status == 'completed' else "yellow" if hasattr(job_status, 'status') and job_status.status in ['running', 'pending'] else "red"
377
+
378
+ # Basic job information
379
+ basic_info = f"""[bold cyan]Job Information:[/bold cyan]
380
+ • Job ID: [white]{job_id}[/white]
381
+ • Status: [{status_color}]{status_emoji} {job_status.status if hasattr(job_status, 'status') else 'Unknown'}[/{status_color}]
382
+ • Job Type: [blue]{job_status.job_type if hasattr(job_status, 'job_type') else 'Unknown'}[/blue]
383
+ • Check ID: [white]{job_status.check_id if hasattr(job_status, 'check_id') else 'None (custom check)'}[/white]
384
+ • Created: [white]{job_status.created_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(job_status, 'created_at') and job_status.created_at else 'Unknown'}[/white]
385
+ • Started: [white]{job_status.started_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(job_status, 'started_at') and job_status.started_at else 'Not started'}[/white]
386
+ • Completed: [white]{job_status.completed_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(job_status, 'completed_at') and job_status.completed_at else 'Not completed'}[/white]"""
387
+
388
+ # Check parameters section
389
+ params_info = ""
390
+ if hasattr(job_status, 'check_parameters') and job_status.check_parameters:
391
+ params = job_status.check_parameters
392
+ params_info = f"""
393
+ [bold cyan]Check Parameters:[/bold cyan]
394
+ • Name: [white]{params.get('name', 'Unknown')}[/white]
395
+ • Type: [blue]{params.get('type', 'Unknown')}[/blue]"""
396
+
397
+ # Add type-specific parameters
398
+ if params.get('type') == 'web':
399
+ if 'url' in params:
400
+ params_info += f"\n• URL: [yellow]{params['url']}[/yellow]"
401
+ elif params.get('type') == 'tcp':
402
+ if 'host' in params:
403
+ params_info += f"\n• Host: [yellow]{params['host']}[/yellow]"
404
+ if 'port' in params:
405
+ params_info += f"\n• Port: [white]{params['port']}[/white]"
406
+ elif params.get('type') in ['multistep', 'synthetic']:
407
+ if 'parameters' in params and 'pw_script' in params['parameters']:
408
+ script_preview = params['parameters']['pw_script']
409
+ if len(script_preview) > 100:
410
+ script_preview = script_preview[:100] + "..."
411
+ params_info += f"\n• Script: [dim]{script_preview}[/dim]"
412
+
413
+ if 'timeout' in params:
414
+ params_info += f"\n• Timeout: [white]{params['timeout']}s[/white]"
415
+
416
+ # Show regions if available
417
+ if 'parameters' in params and isinstance(params['parameters'], dict) and 'regions' in params['parameters']:
418
+ regions = params['parameters']['regions']
419
+ params_info += f"\n• Regions: [white]{', '.join(regions)}[/white]"
420
+
421
+ # Error information
422
+ error_info = ""
423
+ if hasattr(job_status, 'error_message') and job_status.error_message:
424
+ error_info = f"""
425
+ [bold red]Error Information:[/bold red]
426
+ • Error: [red]{job_status.error_message}[/red]"""
427
+
428
+ # Result analysis
429
+ result_info = ""
430
+ if hasattr(job_status, 'result') and job_status.result:
431
+ result = job_status.result
432
+
433
+ # Basic result info
434
+ result_status = result.get('status', 'Unknown') if isinstance(result, dict) else 'Unknown'
435
+ result_status_color = "green" if result_status == 'ok' else "red" if result_status == 'failed' else "yellow"
436
+
437
+ result_info = f"""
438
+ [bold cyan]Result Summary:[/bold cyan]
439
+ • Result Status: [{result_status_color}]{result_status}[/{result_status_color}]"""
440
+
441
+ if isinstance(result, dict):
442
+ # Response time
443
+ if 'response_time' in result and result['response_time']:
444
+ result_info += f"\n• Response Time: [yellow]{result['response_time']}ms[/yellow]"
445
+
446
+ # Check server info
447
+ if 'check_server' in result and result['check_server']:
448
+ server = result['check_server']
449
+ result_info += f"\n• Server: [magenta]{server.get('region', 'Unknown')} ({server.get('ip_address', 'Unknown IP')})[/magenta]"
450
+
451
+ # Error message from result
452
+ if 'error_message' in result and result['error_message']:
453
+ result_info += f"\n• Result Error: [red]{result['error_message']}[/red]"
454
+
455
+ # Format check metadata using existing formatters
456
+ if 'check_metadata' in result and result['check_metadata']:
457
+ metadata = result['check_metadata']
458
+
459
+ # Use the same metadata formatting as check results
460
+ try:
461
+ from ..formatters.registry import FormatterRegistry
462
+ registry = FormatterRegistry(verbose=False) # Default to non-verbose for job status
463
+ metadata_formatted = registry.format_metadata(metadata)
464
+
465
+ if metadata_formatted.strip():
466
+ result_info += f"\n{metadata_formatted}"
467
+ except Exception:
468
+ # Fallback to basic metadata display
469
+ if 'logs' in metadata and metadata['logs']:
470
+ logs = metadata['logs']
471
+ result_info += f"\n• Logs: [dim]{len(logs)} log entries[/dim]"
472
+ # Show first log entry if available
473
+ if logs and len(logs) > 0:
474
+ first_log = logs[0]
475
+ log_level = first_log.get('level', 'info')
476
+ log_message = first_log.get('message', 'No message')
477
+ log_color = "red" if log_level == 'error' else "yellow" if log_level == 'warn' else "white"
478
+ result_info += f"\n - [{log_color}]{log_level.upper()}: {log_message}[/{log_color}]"
479
+
480
+ if 'execution_time' in metadata:
481
+ result_info += f"\n• Execution Time: [white]{metadata['execution_time']}ms[/white]"
482
+
483
+ if 'test_summary' in metadata:
484
+ summary = metadata['test_summary']
485
+ total_tests = summary.get('total', 0)
486
+ passed_tests = summary.get('passed', 0)
487
+ failed_tests = summary.get('failed', 0)
488
+ result_info += f"\n• Test Results: [green]{passed_tests} passed[/green], [red]{failed_tests} failed[/red], [white]{total_tests} total[/white]"
489
+
490
+ # Combine all sections
491
+ full_info = basic_info
492
+ if params_info:
493
+ full_info += params_info
494
+ if error_info:
495
+ full_info += error_info
496
+ if result_info:
497
+ full_info += result_info
498
+
499
+ panel = Panel(
500
+ full_info,
501
+ title=f"📋 Job Status: {job_id}",
502
+ border_style="blue",
503
+ padding=(1, 2),
504
+ )
505
+
506
+ self.console.print(panel)
507
+
508
+ def _wait_and_show_result(self, job_id: str, initial_details: list):
509
+ """Wait for job completion and display the result"""
510
+ import time
511
+ from rich.progress import Progress, SpinnerColumn, TextColumn
512
+
513
+ if self.output_format not in ['json', 'yaml']:
514
+ # Show initial success message
515
+ self.display_success(
516
+ f"On-demand check queued successfully!\n" + "\n".join(initial_details) + f"\n\nWaiting for result...",
517
+ "✅ Check Queued"
518
+ )
519
+
520
+ # Polling logic with progress indicator
521
+ max_wait_time = 300 # 5 minutes maximum wait
522
+ poll_interval = 2 # Poll every 2 seconds
523
+ elapsed_time = 0
524
+
525
+ with Progress(
526
+ SpinnerColumn(),
527
+ TextColumn("[progress.description]{task.description}"),
528
+ console=self.console,
529
+ transient=True
530
+ ) as progress:
531
+ if self.output_format not in ['json', 'yaml']:
532
+ task = progress.add_task("⏳ Waiting for job completion...", total=None)
533
+
534
+ while elapsed_time < max_wait_time:
535
+ try:
536
+ checks_api = self.get_client()
537
+ job_status = checks_api.v1_checks_jobs_job_id_get(job_id=job_id)
538
+
539
+ if hasattr(job_status, 'status'):
540
+ if job_status.status in ['completed', 'failed', 'error']:
541
+ # Job is finished, show the result
542
+ if self.output_format not in ['json', 'yaml']:
543
+ progress.update(task, description=f"✅ Job {job_status.status}!")
544
+ time.sleep(0.5) # Brief pause to show completion
545
+
546
+ # Display the result using existing method
547
+ self.get_job_status(job_id)
548
+ return
549
+
550
+ elif job_status.status == 'running':
551
+ if self.output_format not in ['json', 'yaml']:
552
+ progress.update(task, description=f"🏃 Job running... ({elapsed_time}s elapsed)")
553
+
554
+ else: # pending, queued, etc.
555
+ if self.output_format not in ['json', 'yaml']:
556
+ progress.update(task, description=f"⏳ Job {job_status.status}... ({elapsed_time}s elapsed)")
557
+
558
+ time.sleep(poll_interval)
559
+ elapsed_time += poll_interval
560
+
561
+ except Exception as e:
562
+ if self.output_format in ['json', 'yaml']:
563
+ self.output_data({
564
+ "error": f"Failed to poll job status: {str(e)}",
565
+ "job_id": job_id,
566
+ "elapsed_time": elapsed_time
567
+ })
568
+ else:
569
+ self.display_error(f"Error polling job status: {str(e)}")
570
+ self.display_info(f"You can manually check status with: pngr checks jobs result {job_id}")
571
+ return
572
+
573
+ # Timeout reached
574
+ if self.output_format in ['json', 'yaml']:
575
+ self.output_data({
576
+ "timeout": True,
577
+ "message": f"Job did not complete within {max_wait_time} seconds",
578
+ "job_id": job_id,
579
+ "elapsed_time": elapsed_time
580
+ })
581
+ else:
582
+ self.display_warning(
583
+ f"Job did not complete within {max_wait_time} seconds.\n"
584
+ f"The job may still be running. Use 'pngr checks jobs result {job_id}' to check status manually."
585
+ )
586
+
587
+
588
+ # Create Typer app for on-demand checks commands
589
+ run_app = typer.Typer(name="run", help="🚀 Execute checks on demand")
590
+ jobs_app = typer.Typer(name="jobs", help="📋 Manage check jobs")
591
+
592
+
593
+ def get_output_format():
594
+ """Get output format from config"""
595
+ from ..utils.config import get_config
596
+ return get_config().get('output_format', 'table')
597
+
598
+
599
+ @run_app.command("custom")
600
+ def run_custom_check(
601
+ url: Optional[str] = typer.Option(None, "--url", "-u", help="URL to monitor (required for web, api, ssl checks)"),
602
+ check_type: str = typer.Option("web", "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
603
+ host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL checks (max 255 characters)"),
604
+ port: Optional[int] = typer.Option(None, "--port", help="Port number for TCP checks (1-65535)"),
605
+ timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
606
+ name: str = typer.Option("On-demand check", "--name", "-n", help="Check name"),
607
+ parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
608
+ pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
609
+ wait_for_result: bool = typer.Option(False, "--wait-for-result", help="Wait for job completion and show result immediately (max 5 minutes)"),
610
+ ):
611
+ """Execute custom on-demand check. Parameters vary by check type:
612
+ - web/api: --url required
613
+ - tcp: --host required, --port optional
614
+ - ssl: --url or --host required
615
+ - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
616
+ on_demand_cmd = OnDemandChecksCommand(get_output_format())
617
+ on_demand_cmd.execute_custom_check(url, check_type, host, port, timeout, name, parameters, pw_script_file, wait_for_result)
618
+
619
+
620
+ @run_app.command("existing")
621
+ def run_existing_check(
622
+ check_id: str = typer.Argument(..., help="Existing check ID to execute"),
623
+ wait_for_result: bool = typer.Option(False, "--wait-for-result", help="Wait for job completion and show result immediately (max 5 minutes)"),
624
+ ):
625
+ """Execute existing check on demand"""
626
+ on_demand_cmd = OnDemandChecksCommand(get_output_format())
627
+ on_demand_cmd.execute_existing_check(check_id, wait_for_result)
628
+
629
+
630
+ @jobs_app.command("list")
631
+ def list_jobs(
632
+ page: int = typer.Option(1, "--page", "-p", help="Page number"),
633
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page"),
634
+ ):
635
+ """List check jobs"""
636
+ on_demand_cmd = OnDemandChecksCommand(get_output_format())
637
+ on_demand_cmd.list_jobs(page, page_size)
638
+
639
+
640
+ @jobs_app.command("result")
641
+ def get_job_result(
642
+ job_id: str = typer.Argument(..., help="Job ID to get result for"),
643
+ ):
644
+ """Get job result"""
645
+ on_demand_cmd = OnDemandChecksCommand(get_output_format())
646
+ on_demand_cmd.get_job_status(job_id)
@@ -0,0 +1,13 @@
1
+
2
+ """
3
+ Formatters for different check result types
4
+ """
5
+
6
+ from .ssl_formatter import SSLFormatter
7
+ from .synthetic_formatter import SyntheticFormatter
8
+ from .multistep_formatter import MultistepFormatter
9
+ from .web_formatter import WebFormatter
10
+ from .generic_formatter import GenericFormatter
11
+ from .registry import FormatterRegistry
12
+
13
+ __all__ = ['SSLFormatter', 'SyntheticFormatter', 'MultistepFormatter', 'WebFormatter', 'GenericFormatter', 'FormatterRegistry']