pingera-cli 0.1.12__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.12/pingera_cli.egg-info → pingera_cli-0.1.13}/PKG-INFO +2 -2
  2. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/__init__.py +1 -1
  3. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/__main__.py +1 -2
  4. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/auth.py +3 -1
  5. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/check_groups.py +2 -1
  6. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/check_secrets.py +2 -1
  7. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/checks.py +101 -58
  8. pingera_cli-0.1.13/pingera_cli/commands/execution_groups.py +423 -0
  9. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/on_demand_checks.py +191 -12
  10. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/secrets.py +2 -1
  11. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/main.py +11 -1
  12. {pingera_cli-0.1.12 → pingera_cli-0.1.13/pingera_cli.egg-info}/PKG-INFO +2 -2
  13. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli.egg-info/SOURCES.txt +1 -0
  14. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli.egg-info/requires.txt +1 -1
  15. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pyproject.toml +1 -1
  16. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/.gitignore +0 -0
  17. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/.replit +0 -0
  18. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/LICENSE +0 -0
  19. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/README.md +0 -0
  20. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/assert +0 -0
  21. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/docs/cli-commands.md +0 -0
  22. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/docs/example-check.yaml +0 -0
  23. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/docs/example-synthetic-screenshot.json +0 -0
  24. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/docs/example-web-check.json +0 -0
  25. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/__init__.py +0 -0
  26. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/commands/base.py +0 -0
  27. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/__init__.py +0 -0
  28. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/base_formatter.py +0 -0
  29. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/dns_formatter.py +0 -0
  30. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/generic_formatter.py +0 -0
  31. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/icmp_formatter.py +0 -0
  32. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/multistep_formatter.py +0 -0
  33. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/registry.py +0 -0
  34. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/ssl_formatter.py +0 -0
  35. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/synthetic_formatter.py +0 -0
  36. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/formatters/web_formatter.py +0 -0
  37. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/utils/__init__.py +0 -0
  38. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/utils/config.py +0 -0
  39. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/utils/console.py +0 -0
  40. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli/utils/file_utils.py +0 -0
  41. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli.egg-info/dependency_links.txt +0 -0
  42. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli.egg-info/entry_points.txt +0 -0
  43. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/pingera_cli.egg-info/top_level.txt +0 -0
  44. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/replit.md +0 -0
  45. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/run_tests.py +0 -0
  46. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/setup.cfg +0 -0
  47. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/setup.py +0 -0
  48. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/tests/__init__.py +0 -0
  49. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/tests/conftest.py +0 -0
  50. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/tests/requirements.txt +0 -0
  51. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/tests/test_auth.py +0 -0
  52. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/tests/test_checks.py +0 -0
  53. {pingera_cli-0.1.12 → pingera_cli-0.1.13}/tests/test_config.py +0 -0
  54. {pingera_cli-0.1.12 → 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.12
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.10
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.12"
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
@@ -234,16 +236,16 @@ class ChecksCommand(BaseCommand):
234
236
  )
235
237
 
236
238
  self.console.print(table)
237
-
239
+
238
240
  # Show result summary with filter info
239
241
  summary_parts = [f"Found {len(response.checks)} checks"]
240
242
  if page > 1:
241
243
  summary_parts.append(f"on page {page}")
242
244
  if filters_applied:
243
245
  summary_parts.append(f"with filters: {', '.join(filters_applied)}")
244
-
246
+
245
247
  self.console.print(f"\n[dim]{' '.join(summary_parts)}[/dim]")
246
-
248
+
247
249
  # Show helpful hints for filtering
248
250
  if not any([check_type, status, name, group_id]):
249
251
  self.console.print(f"[dim]💡 Filter checks: --type <type>, --status <status>, --name <name>, --group-id <id>[/dim]")
@@ -300,7 +302,7 @@ class ChecksCommand(BaseCommand):
300
302
  group_name = check.group.name if hasattr(check.group, 'name') else 'Unknown'
301
303
  group_color = check.group.color if hasattr(check.group, 'color') and check.group.color else 'white'
302
304
  group_desc = check.group.description if hasattr(check.group, 'description') and check.group.description else ''
303
-
305
+
304
306
  group_info = f"• Group: [{group_color}]● {group_name}[/{group_color}]"
305
307
  if group_desc:
306
308
  group_info += f" [dim]({group_desc})[/dim]"
@@ -399,13 +401,13 @@ class ChecksCommand(BaseCommand):
399
401
  def _parse_check_file(self, file_path: str) -> dict:
400
402
  """Parse check configuration from JSON or YAML file (local or URL)"""
401
403
  from ..utils.file_utils import load_check_file, is_url
402
-
404
+
403
405
  try:
404
406
  if is_url(file_path):
405
407
  self.display_info(f"Downloading check configuration from: {file_path}")
406
-
408
+
407
409
  return load_check_file(file_path)
408
-
410
+
409
411
  except Exception as e:
410
412
  self.display_error(str(e))
411
413
  raise typer.Exit(1)
@@ -420,12 +422,12 @@ class ChecksCommand(BaseCommand):
420
422
  # If creating from file, parse file and merge with command line options
421
423
  if from_file:
422
424
  file_data = self._parse_check_file(from_file)
423
-
425
+
424
426
  # Filter only SDK-recognized fields from file data
425
427
  # This ignores marketplace-specific fields and other extensions
426
428
  sdk_fields = ["name", "type", "url", "host", "port", "interval", "timeout", "parameters", "active", "secrets"]
427
429
  filtered_file_data = {k: v for k, v in file_data.items() if k in sdk_fields}
428
-
430
+
429
431
  # Command line options take precedence over file data
430
432
  check_data = {
431
433
  "name": filtered_file_data.get("name", name),
@@ -433,7 +435,7 @@ class ChecksCommand(BaseCommand):
433
435
  "interval": filtered_file_data.get("interval", interval),
434
436
  "timeout": filtered_file_data.get("timeout", timeout)
435
437
  }
436
-
438
+
437
439
  # Override with command line values if provided (but only if they were explicitly changed from defaults)
438
440
  # We need to be more careful about when to override file values
439
441
  # For now, just use file values and let user override with explicit CLI options
@@ -441,46 +443,46 @@ class ChecksCommand(BaseCommand):
441
443
  check_data["interval"] = interval
442
444
  if timeout != 30: # Only override if timeout was explicitly provided
443
445
  check_data["timeout"] = timeout
444
-
446
+
445
447
  # Handle URL from file
446
448
  if url is None and filtered_file_data.get("url"):
447
449
  url = filtered_file_data["url"]
448
450
  elif url is not None:
449
451
  # Command line URL takes precedence
450
452
  pass
451
-
453
+
452
454
  # Handle host/port from file
453
455
  if host is None and filtered_file_data.get("host"):
454
456
  host = filtered_file_data["host"]
455
457
  elif host is not None:
456
458
  # Command line host takes precedence
457
459
  pass
458
-
460
+
459
461
  if port is None and filtered_file_data.get("port"):
460
462
  port = filtered_file_data["port"]
461
463
  elif port is not None:
462
464
  # Command line port takes precedence
463
465
  pass
464
-
466
+
465
467
  # Handle parameters from file
466
468
  file_parameters = filtered_file_data.get("parameters")
467
469
  if file_parameters and parameters is None:
468
470
  # Use file parameters if no command line parameters
469
471
  parameters = json.dumps(file_parameters) if isinstance(file_parameters, dict) else file_parameters
470
-
472
+
471
473
  # Handle secrets from file
472
474
  if filtered_file_data.get("secrets"):
473
475
  check_data["secrets"] = filtered_file_data["secrets"]
474
-
476
+
475
477
  # Handle pw_script_file from file (this is CLI-specific, not sent to SDK)
476
478
  if pw_script_file is None and file_data.get("pw_script_file"):
477
479
  pw_script_file = file_data["pw_script_file"]
478
-
480
+
479
481
  # Show info about ignored fields if any
480
482
  ignored_fields = [k for k in file_data.keys() if k not in sdk_fields and k != "pw_script_file"]
481
483
  if ignored_fields:
482
484
  self.display_info(f"Ignoring non-SDK fields from file: {', '.join(ignored_fields)}")
483
-
485
+
484
486
  self.display_info(f"Creating check from file: {from_file}")
485
487
  else:
486
488
  # Build check data normally
@@ -703,7 +705,7 @@ class ChecksCommand(BaseCommand):
703
705
  self.display_error(f"Failed to delete check: {str(e)}")
704
706
  raise typer.Exit(1)
705
707
 
706
- 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):
707
709
  """Get check results using unified results API"""
708
710
  try:
709
711
  unified_api = self.get_unified_results_client()
@@ -718,6 +720,10 @@ class ChecksCommand(BaseCommand):
718
720
  if check_id:
719
721
  params["check_id"] = check_id
720
722
 
723
+ # Add result_id if provided (for fetching specific result)
724
+ if result_id:
725
+ params["result_id"] = result_id
726
+
721
727
  # Add optional filters
722
728
  if from_date:
723
729
  params["start_date"] = from_date
@@ -740,6 +746,36 @@ class ChecksCommand(BaseCommand):
740
746
  self.display_info("No results found.")
741
747
  return
742
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
+
743
779
  # Prepare data for different output formats
744
780
  if self.output_format in ['json', 'yaml']:
745
781
  results_data = []
@@ -868,8 +904,7 @@ class ChecksCommand(BaseCommand):
868
904
  if check_id:
869
905
  hints.append(f"Use 'pngr checks result {check_id} <result-id>' for detailed result info")
870
906
  else:
871
- hints.append("Use 'pngr checks result <check-id> <result-id>' for detailed result info")
872
- 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")
873
908
  hints.append("Use 'pngr checks list' to see all available checks and their IDs")
874
909
  hints.append("Filter results: --type <type>, --status <status>, --region <region>")
875
910
 
@@ -936,7 +971,15 @@ class ChecksCommand(BaseCommand):
936
971
  """Display detailed result information in a rich format"""
937
972
 
938
973
  # Basic information
939
- 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
+
940
983
  basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
941
984
  • Result ID: [white]{result.id}[/white]
942
985
  • Check ID: [white]{result.check_id}[/white]
@@ -1036,7 +1079,7 @@ class ChecksCommand(BaseCommand):
1036
1079
  # Extract region data using actual SDK attributes
1037
1080
  region_id = str(region.id) if hasattr(region, 'id') and region.id else "-"
1038
1081
  display_name = region.display_name if hasattr(region, 'display_name') and region.display_name else "-"
1039
-
1082
+
1040
1083
  # Handle available check types
1041
1084
  available_types = "-"
1042
1085
  if hasattr(region, 'available_check_types') and region.available_check_types:
@@ -1052,7 +1095,7 @@ class ChecksCommand(BaseCommand):
1052
1095
  )
1053
1096
 
1054
1097
  self.console.print(table)
1055
-
1098
+
1056
1099
  # Show summary
1057
1100
  filter_text = f" for {check_type} checks" if check_type else ""
1058
1101
  self.console.print(f"\n[dim]Found {len(response.regions)} regions{filter_text}[/dim]")
@@ -1115,32 +1158,28 @@ class ChecksCommand(BaseCommand):
1115
1158
  app = typer.Typer(name="checks", help="🔍 Manage monitoring checks", no_args_is_help=True)
1116
1159
 
1117
1160
 
1118
- def get_output_format():
1119
- """Get output format from config"""
1120
- from ..utils.config import get_config
1121
- return get_config().get('output_format', 'table')
1122
-
1123
-
1124
1161
  @app.command("list")
1125
1162
  def list_checks(
1126
1163
  page: int = typer.Option(1, "--page", "-p", help="Page number"),
1127
- page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page (max 100)"),
1164
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page"),
1128
1165
  check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by check type (web, api, ssl, tcp, icmp, dns, synthetic, multistep)"),
1129
1166
  status: Optional[str] = typer.Option(None, "--status", help="Filter by status. Multiple statuses can be separated by commas (e.g., 'ok,failed')"),
1130
1167
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name using case-insensitive partial matching (max 100 chars)"),
1131
1168
  group_id: Optional[str] = typer.Option(None, "--group-id", "-g", help="Filter by group ID"),
1132
1169
  ):
1133
1170
  """List monitoring checks with advanced filtering options"""
1171
+ from ..utils.config import get_output_format
1172
+
1134
1173
  # Validate name length
1135
1174
  if name and len(name) > 100:
1136
1175
  typer.echo("Error: Name filter cannot exceed 100 characters", err=True)
1137
1176
  raise typer.Exit(1)
1138
-
1177
+
1139
1178
  # Validate check type
1140
1179
  if check_type and check_type not in ["web", "api", "ssl", "tcp", "icmp", "dns", "synthetic", "multistep"]:
1141
1180
  typer.echo("Error: Invalid check type. Must be one of: web, api, ssl, tcp, icmp, dns, synthetic, multistep", err=True)
1142
1181
  raise typer.Exit(1)
1143
-
1182
+
1144
1183
  # Validate status values if provided
1145
1184
  if status:
1146
1185
  valid_statuses = ["ok", "failed", "degraded", "timeout", "pending", "paused"]
@@ -1149,7 +1188,7 @@ def list_checks(
1149
1188
  if s not in valid_statuses:
1150
1189
  typer.echo(f"Error: Invalid status '{s}'. Must be one of: {', '.join(valid_statuses)}", err=True)
1151
1190
  raise typer.Exit(1)
1152
-
1191
+
1153
1192
  checks_cmd = ChecksCommand(get_output_format())
1154
1193
  checks_cmd.list_checks(page, page_size, check_type, status, name, group_id)
1155
1194
 
@@ -1159,6 +1198,7 @@ def get_check(
1159
1198
  check_id: str = typer.Argument(..., help="Check ID to retrieve"),
1160
1199
  ):
1161
1200
  """Get specific check details"""
1201
+ from ..utils.config import get_output_format
1162
1202
  checks_cmd = ChecksCommand(get_output_format())
1163
1203
  checks_cmd.get_check(check_id)
1164
1204
 
@@ -1177,11 +1217,11 @@ def create_check(
1177
1217
  from_file: Optional[str] = typer.Option(None, "--from-file", "-f", help="Path to JSON or YAML file containing check configuration")
1178
1218
  ):
1179
1219
  """Create a new monitoring check. Can be created from command line options or from a JSON/YAML file.
1180
-
1220
+
1181
1221
  When using --from-file:
1182
1222
  - Command line options override file values
1183
1223
  - File should contain check configuration in JSON or YAML format
1184
-
1224
+
1185
1225
  Parameters vary by check type:
1186
1226
  - web/api: --url required
1187
1227
  - tcp: --host required, --port optional
@@ -1189,12 +1229,13 @@ def create_check(
1189
1229
  - icmp: --host required (hostname or IP to ping)
1190
1230
  - dns: --host required (domain name to query)
1191
1231
  - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
1192
-
1232
+ from ..utils.config import get_output_format
1233
+
1193
1234
  # Validate that we have either from_file or required parameters
1194
1235
  if not from_file and not name:
1195
1236
  typer.echo("Error: --name is required when not using --from-file", err=True)
1196
1237
  raise typer.Exit(1)
1197
-
1238
+
1198
1239
  checks_cmd = ChecksCommand(get_output_format())
1199
1240
  checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file, from_file)
1200
1241
 
@@ -1213,6 +1254,7 @@ def update_check(
1213
1254
  pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks")
1214
1255
  ):
1215
1256
  """Update an existing check. Use --parameters to provide complex parameters like Playwright scripts, regions, etc."""
1257
+ from ..utils.config import get_output_format
1216
1258
  checks_cmd = ChecksCommand(get_output_format())
1217
1259
  checks_cmd.update_check(check_id, name, url, host, port, interval, timeout, active, parameters, pw_script_file)
1218
1260
 
@@ -1223,6 +1265,7 @@ def delete_check(
1223
1265
  confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"),
1224
1266
  ):
1225
1267
  """Delete a monitoring check"""
1268
+ from ..utils.config import get_output_format
1226
1269
  checks_cmd = ChecksCommand(get_output_format())
1227
1270
  checks_cmd.delete_check(check_id, confirm)
1228
1271
 
@@ -1237,10 +1280,12 @@ def get_results(
1237
1280
  status: Optional[str] = typer.Option(None, "--status", help="Filter by status (ok, failed, degraded, timeout, pending)"),
1238
1281
  check_type: Optional[str] = typer.Option(None, "--type", help="Filter by check type (web, api, tcp, ssl, synthetic, multistep)"),
1239
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"),
1240
1284
  ):
1241
- """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
1242
1287
  checks_cmd = ChecksCommand(get_output_format())
1243
- 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)
1244
1289
 
1245
1290
 
1246
1291
  @app.command("result")
@@ -1250,6 +1295,7 @@ def get_result(
1250
1295
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full detailed information including network requests"),
1251
1296
  ):
1252
1297
  """Get detailed information for a specific check result"""
1298
+ from ..utils.config import get_output_format
1253
1299
  checks_cmd = ChecksCommand(get_output_format())
1254
1300
  checks_cmd.get_check_result_detailed(check_id, result_id, verbose)
1255
1301
 
@@ -1259,6 +1305,7 @@ def list_regions(
1259
1305
  check_type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter regions by check type (web, api, tcp, ssl, synthetic, multistep)"),
1260
1306
  ):
1261
1307
  """List available regions for monitoring checks"""
1308
+ from ..utils.config import get_output_format
1262
1309
  checks_cmd = ChecksCommand(get_output_format())
1263
1310
  checks_cmd.list_regions(check_type)
1264
1311
 
@@ -1269,19 +1316,11 @@ def assign_check_to_group(
1269
1316
  group_id: Optional[str] = typer.Option(None, "--group-id", "-g", help="Group ID to assign check to (use 'null' to remove from group)"),
1270
1317
  ):
1271
1318
  """Assign a check to a group or remove it from a group"""
1319
+ from ..utils.config import get_output_format
1272
1320
  checks_cmd = ChecksCommand(get_output_format())
1273
1321
  checks_cmd.assign_check_to_group(check_id, group_id)
1274
1322
 
1275
1323
 
1276
-
1277
-
1278
-
1279
-
1280
-
1281
-
1282
-
1283
-
1284
-
1285
1324
  # Import on-demand checks functionality from separate module
1286
1325
  from .on_demand_checks import run_app, jobs_app
1287
1326
 
@@ -1291,8 +1330,12 @@ from .check_groups import app as groups_app
1291
1330
  # Import check secrets functionality
1292
1331
  from .check_secrets import app as check_secrets_app
1293
1332
 
1294
- # 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
1295
1337
  app.add_typer(run_app, name="run")
1296
1338
  app.add_typer(jobs_app, name="jobs")
1297
1339
  app.add_typer(groups_app, name="groups")
1298
- 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")