pingera-cli 0.1.11__tar.gz → 0.1.13__tar.gz

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.
Files changed (54) hide show
  1. {pingera_cli-0.1.11/pingera_cli.egg-info → pingera_cli-0.1.13}/PKG-INFO +2 -2
  2. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/__init__.py +1 -1
  3. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/__main__.py +1 -2
  4. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/auth.py +3 -1
  5. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/check_groups.py +2 -1
  6. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/check_secrets.py +2 -1
  7. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/checks.py +118 -66
  8. pingera_cli-0.1.13/pingera_cli/commands/execution_groups.py +423 -0
  9. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/on_demand_checks.py +191 -12
  10. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/secrets.py +2 -1
  11. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/__init__.py +3 -1
  12. pingera_cli-0.1.13/pingera_cli/formatters/dns_formatter.py +66 -0
  13. pingera_cli-0.1.13/pingera_cli/formatters/icmp_formatter.py +64 -0
  14. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/registry.py +4 -0
  15. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/main.py +11 -1
  16. {pingera_cli-0.1.11 → pingera_cli-0.1.13/pingera_cli.egg-info}/PKG-INFO +2 -2
  17. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli.egg-info/SOURCES.txt +3 -0
  18. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli.egg-info/requires.txt +1 -1
  19. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pyproject.toml +1 -1
  20. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/.gitignore +0 -0
  21. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/.replit +0 -0
  22. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/LICENSE +0 -0
  23. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/README.md +0 -0
  24. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/assert +0 -0
  25. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/docs/cli-commands.md +0 -0
  26. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/docs/example-check.yaml +0 -0
  27. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/docs/example-synthetic-screenshot.json +0 -0
  28. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/docs/example-web-check.json +0 -0
  29. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/__init__.py +0 -0
  30. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/commands/base.py +0 -0
  31. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/base_formatter.py +0 -0
  32. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/generic_formatter.py +0 -0
  33. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/multistep_formatter.py +0 -0
  34. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/ssl_formatter.py +0 -0
  35. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/synthetic_formatter.py +0 -0
  36. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/formatters/web_formatter.py +0 -0
  37. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/utils/__init__.py +0 -0
  38. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/utils/config.py +0 -0
  39. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/utils/console.py +0 -0
  40. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli/utils/file_utils.py +0 -0
  41. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli.egg-info/dependency_links.txt +0 -0
  42. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli.egg-info/entry_points.txt +0 -0
  43. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/pingera_cli.egg-info/top_level.txt +0 -0
  44. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/replit.md +0 -0
  45. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/run_tests.py +0 -0
  46. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/setup.cfg +0 -0
  47. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/setup.py +0 -0
  48. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/__init__.py +0 -0
  49. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/conftest.py +0 -0
  50. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/requirements.txt +0 -0
  51. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/test_auth.py +0 -0
  52. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/test_checks.py +0 -0
  53. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/test_config.py +0 -0
  54. {pingera_cli-0.1.11 → pingera_cli-0.1.13}/tests/test_on_demand_checks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingera-cli
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: A beautiful Python CLI tool built with typer and rich, distributed via pip and based on Pingera SDK
5
5
  Home-page: https://github.com/pingera/pingera-cli
6
6
  Author: PingeraCLI Team
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
31
31
  License-File: LICENSE
32
32
  Requires-Dist: typer[all]>=0.9.0
33
33
  Requires-Dist: rich>=13.0.0
34
- Requires-Dist: pingera-sdk>=1.0.8
34
+ Requires-Dist: pingera-sdk>=1.0.11
35
35
  Requires-Dist: click>=8.0.0
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: pytest>=6.0; extra == "dev"
@@ -2,7 +2,7 @@
2
2
  Pingera CLI - A beautiful CLI tool for Pingera Platform
3
3
  """
4
4
 
5
- __version__ = "0.1.11"
5
+ __version__ = "0.1.13"
6
6
  __author__ = "Pingera Team"
7
7
  __description__ = "A beautiful Python CLI tool built with typer and rich, distributed via pip and based on Pingera SDK"
8
8
 
@@ -3,7 +3,6 @@
3
3
  Main module entry point for python -m pingera_cli execution
4
4
  """
5
5
 
6
- from .main import cli_entry_point
7
-
8
6
  if __name__ == "__main__":
7
+ from .main import cli_entry_point
9
8
  cli_entry_point()
@@ -224,8 +224,10 @@ API Key: ✗ Not set
224
224
  from pingera.exceptions import UnauthorizedException
225
225
 
226
226
  # Configure the client (same pattern as checks command)
227
+ from ..utils.config import get_config
228
+
227
229
  configuration = Configuration()
228
- configuration.host = "https://api.pingera.ru"
230
+ configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
229
231
  configuration.api_key['apiKeyAuth'] = api_key
230
232
 
231
233
  # Create API client
@@ -33,10 +33,11 @@ class CheckGroupsCommand(BaseCommand):
33
33
  try:
34
34
  from pingera import ApiClient, Configuration
35
35
  from pingera.api import CheckGroupsApi
36
+ from ..utils.config import get_config
36
37
 
37
38
  # Configure the client
38
39
  configuration = Configuration()
39
- configuration.host = "https://api.pingera.ru"
40
+ configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
40
41
  configuration.api_key['apiKeyAuth'] = api_key
41
42
 
42
43
  # Create API client
@@ -34,10 +34,11 @@ class CheckSecretsCommand(BaseCommand):
34
34
  try:
35
35
  from pingera import ApiClient, Configuration
36
36
  from pingera.api import CheckSecretsApi
37
+ from ..utils.config import get_config
37
38
 
38
39
  # Configure the client
39
40
  configuration = Configuration()
40
- configuration.host = "https://api.pingera.ru"
41
+ configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
41
42
  configuration.api_key['apiKeyAuth'] = api_key
42
43
 
43
44
  # Create API client
@@ -33,10 +33,11 @@ class ChecksCommand(BaseCommand):
33
33
  try:
34
34
  from pingera import ApiClient, Configuration
35
35
  from pingera.api import ChecksApi
36
+ from ..utils.config import get_config
36
37
 
37
38
  # Configure the client
38
39
  configuration = Configuration()
39
- configuration.host = "https://api.pingera.ru"
40
+ configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
40
41
  configuration.api_key['apiKeyAuth'] = api_key
41
42
 
42
43
  # Create API client
@@ -59,10 +60,11 @@ class ChecksCommand(BaseCommand):
59
60
  try:
60
61
  from pingera import ApiClient, Configuration
61
62
  from pingera.api import ChecksUnifiedResultsApi
63
+ from ..utils.config import get_config
62
64
 
63
65
  # Configure the client
64
66
  configuration = Configuration()
65
- configuration.host = "https://api.pingera.ru"
67
+ configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
66
68
  configuration.api_key['apiKeyAuth'] = api_key
67
69
 
68
70
  # Create API client
@@ -85,7 +87,7 @@ class ChecksCommand(BaseCommand):
85
87
  "page": page,
86
88
  "page_size": page_size
87
89
  }
88
-
90
+
89
91
  # Add optional filters
90
92
  if check_type:
91
93
  params["type"] = check_type
@@ -148,12 +150,12 @@ class ChecksCommand(BaseCommand):
148
150
  filters_applied.append(f"name='{name}'")
149
151
  if group_id:
150
152
  filters_applied.append(f"group={group_id}")
151
-
153
+
152
154
  if filters_applied:
153
155
  title_parts.append(f"({', '.join(filters_applied)})")
154
-
156
+
155
157
  table_title = " ".join(title_parts)
156
-
158
+
157
159
  # Create table for default output
158
160
  table = Table(title=table_title)
159
161
  table.add_column("ID", style="cyan")
@@ -181,7 +183,7 @@ class ChecksCommand(BaseCommand):
181
183
  group_display = group_name[:19] + "…"
182
184
  else:
183
185
  group_display = group_name
184
-
186
+
185
187
  # Add color if available
186
188
  if hasattr(check.group, 'color') and check.group.color:
187
189
  # Use the group color for the group name display
@@ -194,8 +196,15 @@ class ChecksCommand(BaseCommand):
194
196
  check_url = "-"
195
197
  if hasattr(check, 'url') and check.url:
196
198
  check_url = str(check.url)
197
- elif hasattr(check, 'host') and hasattr(check, 'port') and check.host and check.port:
198
- check_url = f"{check.host}:{check.port}"
199
+ elif hasattr(check, 'host') and check.host:
200
+ # For ICMP and DNS checks, show just the host
201
+ # For TCP and other checks with ports, show host:port
202
+ if check_type in ["icmp", "dns"]:
203
+ check_url = str(check.host)
204
+ elif hasattr(check, 'port') and check.port:
205
+ check_url = f"{check.host}:{check.port}"
206
+ else:
207
+ check_url = str(check.host)
199
208
 
200
209
  # Handle active status
201
210
  active_status = "✅" if hasattr(check, 'active') and check.active else "❌"
@@ -227,16 +236,16 @@ class ChecksCommand(BaseCommand):
227
236
  )
228
237
 
229
238
  self.console.print(table)
230
-
239
+
231
240
  # Show result summary with filter info
232
241
  summary_parts = [f"Found {len(response.checks)} checks"]
233
242
  if page > 1:
234
243
  summary_parts.append(f"on page {page}")
235
244
  if filters_applied:
236
245
  summary_parts.append(f"with filters: {', '.join(filters_applied)}")
237
-
246
+
238
247
  self.console.print(f"\n[dim]{' '.join(summary_parts)}[/dim]")
239
-
248
+
240
249
  # Show helpful hints for filtering
241
250
  if not any([check_type, status, name, group_id]):
242
251
  self.console.print(f"[dim]💡 Filter checks: --type <type>, --status <status>, --name <name>, --group-id <id>[/dim]")
@@ -293,7 +302,7 @@ class ChecksCommand(BaseCommand):
293
302
  group_name = check.group.name if hasattr(check.group, 'name') else 'Unknown'
294
303
  group_color = check.group.color if hasattr(check.group, 'color') and check.group.color else 'white'
295
304
  group_desc = check.group.description if hasattr(check.group, 'description') and check.group.description else ''
296
-
305
+
297
306
  group_info = f"• Group: [{group_color}]● {group_name}[/{group_color}]"
298
307
  if group_desc:
299
308
  group_info += f" [dim]({group_desc})[/dim]"
@@ -392,13 +401,13 @@ class ChecksCommand(BaseCommand):
392
401
  def _parse_check_file(self, file_path: str) -> dict:
393
402
  """Parse check configuration from JSON or YAML file (local or URL)"""
394
403
  from ..utils.file_utils import load_check_file, is_url
395
-
404
+
396
405
  try:
397
406
  if is_url(file_path):
398
407
  self.display_info(f"Downloading check configuration from: {file_path}")
399
-
408
+
400
409
  return load_check_file(file_path)
401
-
410
+
402
411
  except Exception as e:
403
412
  self.display_error(str(e))
404
413
  raise typer.Exit(1)
@@ -413,12 +422,12 @@ class ChecksCommand(BaseCommand):
413
422
  # If creating from file, parse file and merge with command line options
414
423
  if from_file:
415
424
  file_data = self._parse_check_file(from_file)
416
-
425
+
417
426
  # Filter only SDK-recognized fields from file data
418
427
  # This ignores marketplace-specific fields and other extensions
419
428
  sdk_fields = ["name", "type", "url", "host", "port", "interval", "timeout", "parameters", "active", "secrets"]
420
429
  filtered_file_data = {k: v for k, v in file_data.items() if k in sdk_fields}
421
-
430
+
422
431
  # Command line options take precedence over file data
423
432
  check_data = {
424
433
  "name": filtered_file_data.get("name", name),
@@ -426,7 +435,7 @@ class ChecksCommand(BaseCommand):
426
435
  "interval": filtered_file_data.get("interval", interval),
427
436
  "timeout": filtered_file_data.get("timeout", timeout)
428
437
  }
429
-
438
+
430
439
  # Override with command line values if provided (but only if they were explicitly changed from defaults)
431
440
  # We need to be more careful about when to override file values
432
441
  # For now, just use file values and let user override with explicit CLI options
@@ -434,46 +443,46 @@ class ChecksCommand(BaseCommand):
434
443
  check_data["interval"] = interval
435
444
  if timeout != 30: # Only override if timeout was explicitly provided
436
445
  check_data["timeout"] = timeout
437
-
446
+
438
447
  # Handle URL from file
439
448
  if url is None and filtered_file_data.get("url"):
440
449
  url = filtered_file_data["url"]
441
450
  elif url is not None:
442
451
  # Command line URL takes precedence
443
452
  pass
444
-
453
+
445
454
  # Handle host/port from file
446
455
  if host is None and filtered_file_data.get("host"):
447
456
  host = filtered_file_data["host"]
448
457
  elif host is not None:
449
458
  # Command line host takes precedence
450
459
  pass
451
-
460
+
452
461
  if port is None and filtered_file_data.get("port"):
453
462
  port = filtered_file_data["port"]
454
463
  elif port is not None:
455
464
  # Command line port takes precedence
456
465
  pass
457
-
466
+
458
467
  # Handle parameters from file
459
468
  file_parameters = filtered_file_data.get("parameters")
460
469
  if file_parameters and parameters is None:
461
470
  # Use file parameters if no command line parameters
462
471
  parameters = json.dumps(file_parameters) if isinstance(file_parameters, dict) else file_parameters
463
-
472
+
464
473
  # Handle secrets from file
465
474
  if filtered_file_data.get("secrets"):
466
475
  check_data["secrets"] = filtered_file_data["secrets"]
467
-
476
+
468
477
  # Handle pw_script_file from file (this is CLI-specific, not sent to SDK)
469
478
  if pw_script_file is None and file_data.get("pw_script_file"):
470
479
  pw_script_file = file_data["pw_script_file"]
471
-
480
+
472
481
  # Show info about ignored fields if any
473
482
  ignored_fields = [k for k in file_data.keys() if k not in sdk_fields and k != "pw_script_file"]
474
483
  if ignored_fields:
475
484
  self.display_info(f"Ignoring non-SDK fields from file: {', '.join(ignored_fields)}")
476
-
485
+
477
486
  self.display_info(f"Creating check from file: {from_file}")
478
487
  else:
479
488
  # Build check data normally
@@ -537,7 +546,7 @@ class ChecksCommand(BaseCommand):
537
546
  self.display_error(f"URL is required for {actual_check_type} checks")
538
547
  raise typer.Exit(1)
539
548
 
540
- if actual_check_type in ['tcp'] and not host:
549
+ if actual_check_type in ['tcp', 'icmp', 'dns'] and not host:
541
550
  self.display_error(f"Host is required for {actual_check_type} checks")
542
551
  raise typer.Exit(1)
543
552
 
@@ -696,7 +705,7 @@ class ChecksCommand(BaseCommand):
696
705
  self.display_error(f"Failed to delete check: {str(e)}")
697
706
  raise typer.Exit(1)
698
707
 
699
- 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):
708
+ 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, result_id: Optional[str] = None):
700
709
  """Get check results using unified results API"""
701
710
  try:
702
711
  unified_api = self.get_unified_results_client()
@@ -711,6 +720,10 @@ class ChecksCommand(BaseCommand):
711
720
  if check_id:
712
721
  params["check_id"] = check_id
713
722
 
723
+ # Add result_id if provided (for fetching specific result)
724
+ if result_id:
725
+ params["result_id"] = result_id
726
+
714
727
  # Add optional filters
715
728
  if from_date:
716
729
  params["start_date"] = from_date
@@ -733,6 +746,36 @@ class ChecksCommand(BaseCommand):
733
746
  self.display_info("No results found.")
734
747
  return
735
748
 
749
+ # If result_id was specified, we're fetching a single result - display it with full formatting
750
+ if result_id and len(response.results) == 1:
751
+ result = response.results[0]
752
+
753
+ if self.output_format in ['json', 'yaml']:
754
+ result_dict = {
755
+ "id": str(result.id) if hasattr(result, 'id') and result.id else None,
756
+ "check_id": result.check_id if hasattr(result, 'check_id') else None,
757
+ "check_name": result.check_name if hasattr(result, 'check_name') else None,
758
+ "check_type": result.check_type if hasattr(result, 'check_type') else None,
759
+ "status": result.status if hasattr(result, 'status') else None,
760
+ "created_at": result.created_at if hasattr(result, 'created_at') and result.created_at else None,
761
+ "response_time": result.response_time if hasattr(result, 'response_time') else None,
762
+ "error_message": result.error_message if hasattr(result, 'error_message') else None,
763
+ "check_server_id": result.check_server_id if hasattr(result, 'check_server_id') else None,
764
+ "region": result.region if hasattr(result, 'region') else None,
765
+ "result_type": result.result_type if hasattr(result, 'result_type') else None,
766
+ "check_metadata": result.check_metadata if hasattr(result, 'check_metadata') else None,
767
+ "check_server": {
768
+ "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,
769
+ "country": result.check_server.country if hasattr(result, 'check_server') and result.check_server and hasattr(result.check_server, 'country') else None,
770
+ "region": result.check_server.region if hasattr(result, 'check_server') and result.check_server and hasattr(result.check_server, 'region') else None
771
+ } if hasattr(result, 'check_server') and result.check_server else None
772
+ }
773
+ self.output_data(result_dict)
774
+ else:
775
+ # Display with full formatting using _display_detailed_result
776
+ self._display_detailed_result(result, verbose=True)
777
+ return
778
+
736
779
  # Prepare data for different output formats
737
780
  if self.output_format in ['json', 'yaml']:
738
781
  results_data = []
@@ -861,8 +904,7 @@ class ChecksCommand(BaseCommand):
861
904
  if check_id:
862
905
  hints.append(f"Use 'pngr checks result {check_id} <result-id>' for detailed result info")
863
906
  else:
864
- hints.append("Use 'pngr checks result <check-id> <result-id>' for detailed result info")
865
- hints.append("For on-demand checks: use 'pngr checks jobs result <result-id>' for detailed info")
907
+ hints.append("Use 'pngr checks results --result-id <result-id>' for detailed result info")
866
908
  hints.append("Use 'pngr checks list' to see all available checks and their IDs")
867
909
  hints.append("Filter results: --type <type>, --status <status>, --region <region>")
868
910
 
@@ -929,7 +971,15 @@ class ChecksCommand(BaseCommand):
929
971
  """Display detailed result information in a rich format"""
930
972
 
931
973
  # Basic information
932
- response_time_display = f"{result.response_time}ms" if hasattr(result, 'response_time') and result.response_time else 'N/A'
974
+ # Handle response time display: 0 means <1ms, None/missing means N/A
975
+ if hasattr(result, 'response_time') and result.response_time is not None:
976
+ if result.response_time == 0:
977
+ response_time_display = "<1ms"
978
+ else:
979
+ response_time_display = f"{result.response_time}ms"
980
+ else:
981
+ response_time_display = 'N/A'
982
+
933
983
  basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
934
984
  • Result ID: [white]{result.id}[/white]
935
985
  • Check ID: [white]{result.check_id}[/white]
@@ -1029,7 +1079,7 @@ class ChecksCommand(BaseCommand):
1029
1079
  # Extract region data using actual SDK attributes
1030
1080
  region_id = str(region.id) if hasattr(region, 'id') and region.id else "-"
1031
1081
  display_name = region.display_name if hasattr(region, 'display_name') and region.display_name else "-"
1032
-
1082
+
1033
1083
  # Handle available check types
1034
1084
  available_types = "-"
1035
1085
  if hasattr(region, 'available_check_types') and region.available_check_types:
@@ -1045,7 +1095,7 @@ class ChecksCommand(BaseCommand):
1045
1095
  )
1046
1096
 
1047
1097
  self.console.print(table)
1048
-
1098
+
1049
1099
  # Show summary
1050
1100
  filter_text = f" for {check_type} checks" if check_type else ""
1051
1101
  self.console.print(f"\n[dim]Found {len(response.regions)} regions{filter_text}[/dim]")
@@ -1108,32 +1158,28 @@ class ChecksCommand(BaseCommand):
1108
1158
  app = typer.Typer(name="checks", help="🔍 Manage monitoring checks", no_args_is_help=True)
1109
1159
 
1110
1160
 
1111
- def get_output_format():
1112
- """Get output format from config"""
1113
- from ..utils.config import get_config
1114
- return get_config().get('output_format', 'table')
1115
-
1116
-
1117
1161
  @app.command("list")
1118
1162
  def list_checks(
1119
1163
  page: int = typer.Option(1, "--page", "-p", help="Page number"),
1120
- page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page (max 100)"),
1121
- check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by check type (web, api, ssl, tcp, synthetic, multistep)"),
1164
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page"),
1165
+ check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by check type (web, api, ssl, tcp, icmp, dns, synthetic, multistep)"),
1122
1166
  status: Optional[str] = typer.Option(None, "--status", help="Filter by status. Multiple statuses can be separated by commas (e.g., 'ok,failed')"),
1123
1167
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name using case-insensitive partial matching (max 100 chars)"),
1124
1168
  group_id: Optional[str] = typer.Option(None, "--group-id", "-g", help="Filter by group ID"),
1125
1169
  ):
1126
1170
  """List monitoring checks with advanced filtering options"""
1171
+ from ..utils.config import get_output_format
1172
+
1127
1173
  # Validate name length
1128
1174
  if name and len(name) > 100:
1129
1175
  typer.echo("Error: Name filter cannot exceed 100 characters", err=True)
1130
1176
  raise typer.Exit(1)
1131
-
1177
+
1132
1178
  # Validate check type
1133
- if check_type and check_type not in ["web", "api", "ssl", "tcp", "synthetic", "multistep"]:
1134
- typer.echo("Error: Invalid check type. Must be one of: web, api, ssl, tcp, synthetic, multistep", err=True)
1179
+ if check_type and check_type not in ["web", "api", "ssl", "tcp", "icmp", "dns", "synthetic", "multistep"]:
1180
+ typer.echo("Error: Invalid check type. Must be one of: web, api, ssl, tcp, icmp, dns, synthetic, multistep", err=True)
1135
1181
  raise typer.Exit(1)
1136
-
1182
+
1137
1183
  # Validate status values if provided
1138
1184
  if status:
1139
1185
  valid_statuses = ["ok", "failed", "degraded", "timeout", "pending", "paused"]
@@ -1142,7 +1188,7 @@ def list_checks(
1142
1188
  if s not in valid_statuses:
1143
1189
  typer.echo(f"Error: Invalid status '{s}'. Must be one of: {', '.join(valid_statuses)}", err=True)
1144
1190
  raise typer.Exit(1)
1145
-
1191
+
1146
1192
  checks_cmd = ChecksCommand(get_output_format())
1147
1193
  checks_cmd.list_checks(page, page_size, check_type, status, name, group_id)
1148
1194
 
@@ -1152,6 +1198,7 @@ def get_check(
1152
1198
  check_id: str = typer.Argument(..., help="Check ID to retrieve"),
1153
1199
  ):
1154
1200
  """Get specific check details"""
1201
+ from ..utils.config import get_output_format
1155
1202
  checks_cmd = ChecksCommand(get_output_format())
1156
1203
  checks_cmd.get_check(check_id)
1157
1204
 
@@ -1159,9 +1206,9 @@ def get_check(
1159
1206
  @app.command("create")
1160
1207
  def create_check(
1161
1208
  name: str = typer.Option("", "--name", "-n", help="Check name (required if not using --from-file)"),
1162
- check_type: str = typer.Option("web", "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
1209
+ check_type: str = typer.Option("web", "--type", "-t", help="Check type (web, api, tcp, ssl, icmp, dns, synthetic, multistep)"),
1163
1210
  url: Optional[str] = typer.Option(None, "--url", "-u", help="URL to monitor (required for web, api, ssl checks)"),
1164
- host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL checks (max 255 characters)"),
1211
+ host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL/ICMP/DNS checks (max 255 characters)"),
1165
1212
  port: Optional[int] = typer.Option(None, "--port", help="Port number for TCP checks (1-65535)"),
1166
1213
  interval: int = typer.Option(300, "--interval", "-i", help="Check interval in seconds"),
1167
1214
  timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
@@ -1170,22 +1217,25 @@ def create_check(
1170
1217
  from_file: Optional[str] = typer.Option(None, "--from-file", "-f", help="Path to JSON or YAML file containing check configuration")
1171
1218
  ):
1172
1219
  """Create a new monitoring check. Can be created from command line options or from a JSON/YAML file.
1173
-
1220
+
1174
1221
  When using --from-file:
1175
1222
  - Command line options override file values
1176
1223
  - File should contain check configuration in JSON or YAML format
1177
-
1224
+
1178
1225
  Parameters vary by check type:
1179
1226
  - web/api: --url required
1180
1227
  - tcp: --host required, --port optional
1181
1228
  - ssl: --url or --host required
1229
+ - icmp: --host required (hostname or IP to ping)
1230
+ - dns: --host required (domain name to query)
1182
1231
  - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
1183
-
1232
+ from ..utils.config import get_output_format
1233
+
1184
1234
  # Validate that we have either from_file or required parameters
1185
1235
  if not from_file and not name:
1186
1236
  typer.echo("Error: --name is required when not using --from-file", err=True)
1187
1237
  raise typer.Exit(1)
1188
-
1238
+
1189
1239
  checks_cmd = ChecksCommand(get_output_format())
1190
1240
  checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file, from_file)
1191
1241
 
@@ -1204,6 +1254,7 @@ def update_check(
1204
1254
  pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks")
1205
1255
  ):
1206
1256
  """Update an existing check. Use --parameters to provide complex parameters like Playwright scripts, regions, etc."""
1257
+ from ..utils.config import get_output_format
1207
1258
  checks_cmd = ChecksCommand(get_output_format())
1208
1259
  checks_cmd.update_check(check_id, name, url, host, port, interval, timeout, active, parameters, pw_script_file)
1209
1260
 
@@ -1214,6 +1265,7 @@ def delete_check(
1214
1265
  confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"),
1215
1266
  ):
1216
1267
  """Delete a monitoring check"""
1268
+ from ..utils.config import get_output_format
1217
1269
  checks_cmd = ChecksCommand(get_output_format())
1218
1270
  checks_cmd.delete_check(check_id, confirm)
1219
1271
 
@@ -1228,10 +1280,12 @@ def get_results(
1228
1280
  status: Optional[str] = typer.Option(None, "--status", help="Filter by status (ok, failed, degraded, timeout, pending)"),
1229
1281
  check_type: Optional[str] = typer.Option(None, "--type", help="Filter by check type (web, api, tcp, ssl, synthetic, multistep)"),
1230
1282
  region: Optional[str] = typer.Option(None, "--region", help="Filter by region"),
1283
+ result_id: Optional[str] = typer.Option(None, "--result-id", "-r", help="Filter by specific result ID"),
1231
1284
  ):
1232
- """Get check results with advanced filtering. If no check_id is provided, returns unified results across all checks."""
1285
+ """Get check results with advanced filtering. If no check_id is provided, returns unified results across all checks. Use --result-id to fetch a specific result."""
1286
+ from ..utils.config import get_output_format
1233
1287
  checks_cmd = ChecksCommand(get_output_format())
1234
- checks_cmd.get_check_results(check_id, from_date, to_date, page, page_size, status, check_type, region)
1288
+ checks_cmd.get_check_results(check_id, from_date, to_date, page, page_size, status, check_type, region, result_id)
1235
1289
 
1236
1290
 
1237
1291
  @app.command("result")
@@ -1241,6 +1295,7 @@ def get_result(
1241
1295
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full detailed information including network requests"),
1242
1296
  ):
1243
1297
  """Get detailed information for a specific check result"""
1298
+ from ..utils.config import get_output_format
1244
1299
  checks_cmd = ChecksCommand(get_output_format())
1245
1300
  checks_cmd.get_check_result_detailed(check_id, result_id, verbose)
1246
1301
 
@@ -1250,6 +1305,7 @@ def list_regions(
1250
1305
  check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter regions by check type (web, api, tcp, ssl, synthetic, multistep)"),
1251
1306
  ):
1252
1307
  """List available regions for monitoring checks"""
1308
+ from ..utils.config import get_output_format
1253
1309
  checks_cmd = ChecksCommand(get_output_format())
1254
1310
  checks_cmd.list_regions(check_type)
1255
1311
 
@@ -1260,19 +1316,11 @@ def assign_check_to_group(
1260
1316
  group_id: Optional[str] = typer.Option(None, "--group-id", "-g", help="Group ID to assign check to (use 'null' to remove from group)"),
1261
1317
  ):
1262
1318
  """Assign a check to a group or remove it from a group"""
1319
+ from ..utils.config import get_output_format
1263
1320
  checks_cmd = ChecksCommand(get_output_format())
1264
1321
  checks_cmd.assign_check_to_group(check_id, group_id)
1265
1322
 
1266
1323
 
1267
-
1268
-
1269
-
1270
-
1271
-
1272
-
1273
-
1274
-
1275
-
1276
1324
  # Import on-demand checks functionality from separate module
1277
1325
  from .on_demand_checks import run_app, jobs_app
1278
1326
 
@@ -1282,8 +1330,12 @@ from .check_groups import app as groups_app
1282
1330
  # Import check secrets functionality
1283
1331
  from .check_secrets import app as check_secrets_app
1284
1332
 
1285
- # Add the run, jobs, groups, and secrets subcommands to the main checks app
1333
+ # Import execution groups functionality
1334
+ from .execution_groups import app as execution_groups_app
1335
+
1336
+ # Add the run, jobs, groups, secrets, and execution-groups subcommands to the main checks app
1286
1337
  app.add_typer(run_app, name="run")
1287
1338
  app.add_typer(jobs_app, name="jobs")
1288
1339
  app.add_typer(groups_app, name="groups")
1289
- app.add_typer(check_secrets_app, name="secrets")
1340
+ app.add_typer(check_secrets_app, name="secrets")
1341
+ app.add_typer(execution_groups_app, name="execution-groups")