pingera-cli 0.1.13__tar.gz → 0.1.15__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 (57) hide show
  1. {pingera_cli-0.1.13/pingera_cli.egg-info → pingera_cli-0.1.15}/PKG-INFO +2 -2
  2. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/__init__.py +1 -1
  3. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/checks.py +74 -27
  4. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/on_demand_checks.py +172 -37
  5. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/__init__.py +2 -1
  6. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/base_formatter.py +7 -0
  7. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/multistep_formatter.py +30 -31
  8. pingera_cli-0.1.15/pingera_cli/formatters/portscan_formatter.py +189 -0
  9. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/registry.py +2 -0
  10. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/ssl_formatter.py +77 -27
  11. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/web_formatter.py +37 -32
  12. pingera_cli-0.1.15/pingera_cli/main.py +455 -0
  13. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/utils/config.py +26 -0
  14. {pingera_cli-0.1.13 → pingera_cli-0.1.15/pingera_cli.egg-info}/PKG-INFO +2 -2
  15. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli.egg-info/SOURCES.txt +2 -0
  16. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli.egg-info/requires.txt +1 -1
  17. pingera_cli-0.1.15/pwscript.js +14 -0
  18. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pyproject.toml +1 -1
  19. pingera_cli-0.1.13/pingera_cli/main.py +0 -226
  20. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/.gitignore +0 -0
  21. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/.replit +0 -0
  22. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/LICENSE +0 -0
  23. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/README.md +0 -0
  24. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/assert +0 -0
  25. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/docs/cli-commands.md +0 -0
  26. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/docs/example-check.yaml +0 -0
  27. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/docs/example-synthetic-screenshot.json +0 -0
  28. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/docs/example-web-check.json +0 -0
  29. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/__main__.py +0 -0
  30. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/__init__.py +0 -0
  31. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/auth.py +0 -0
  32. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/base.py +0 -0
  33. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/check_groups.py +0 -0
  34. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/check_secrets.py +0 -0
  35. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/execution_groups.py +0 -0
  36. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/commands/secrets.py +0 -0
  37. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/dns_formatter.py +0 -0
  38. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/generic_formatter.py +0 -0
  39. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/icmp_formatter.py +0 -0
  40. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/formatters/synthetic_formatter.py +0 -0
  41. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/utils/__init__.py +0 -0
  42. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/utils/console.py +0 -0
  43. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli/utils/file_utils.py +0 -0
  44. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli.egg-info/dependency_links.txt +0 -0
  45. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli.egg-info/entry_points.txt +0 -0
  46. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/pingera_cli.egg-info/top_level.txt +0 -0
  47. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/replit.md +0 -0
  48. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/run_tests.py +0 -0
  49. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/setup.cfg +0 -0
  50. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/setup.py +0 -0
  51. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/tests/__init__.py +0 -0
  52. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/tests/conftest.py +0 -0
  53. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/tests/requirements.txt +0 -0
  54. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/tests/test_auth.py +0 -0
  55. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/tests/test_checks.py +0 -0
  56. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/tests/test_config.py +0 -0
  57. {pingera_cli-0.1.13 → pingera_cli-0.1.15}/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.13
3
+ Version: 0.1.15
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.11
34
+ Requires-Dist: pingera-sdk>=1.0.13
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.13"
5
+ __version__ = "0.1.15"
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
 
@@ -14,6 +14,9 @@ from rich.prompt import Confirm
14
14
  from .base import BaseCommand
15
15
  from ..utils.config import get_api_key
16
16
 
17
+ # Supported check types
18
+ SUPPORTED_CHECK_TYPES = ["web", "api", "tcp", "ssl", "dns", "icmp", "portscan", "synthetic", "multistep"]
19
+
17
20
 
18
21
  class ChecksCommand(BaseCommand):
19
22
  """
@@ -412,7 +415,7 @@ class ChecksCommand(BaseCommand):
412
415
  self.display_error(str(e))
413
416
  raise typer.Exit(1)
414
417
 
415
- 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, from_file: Optional[str] = None):
418
+ 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, from_file: Optional[str] = None, regions: Optional[str] = None):
416
419
  """Create a new monitoring check"""
417
420
  try:
418
421
  import json
@@ -525,7 +528,14 @@ class ChecksCommand(BaseCommand):
525
528
  self.display_error(f"Failed to read Playwright script file: {str(e)}")
526
529
  raise typer.Exit(1)
527
530
 
528
- # Parse parameters JSON if provided
531
+ # Handle regions parameter - add to params_dict if provided and not in parameters
532
+ if regions is not None and parameters is None:
533
+ # Parse comma-separated regions into a list
534
+ regions_list = [r.strip() for r in regions.split(',') if r.strip()]
535
+ if regions_list:
536
+ params_dict["regions"] = regions_list
537
+
538
+ # Parse parameters JSON if provided (this takes precedence over --regions)
529
539
  if parameters is not None:
530
540
  try:
531
541
  parsed_params = json.loads(parameters)
@@ -583,7 +593,7 @@ class ChecksCommand(BaseCommand):
583
593
  self.display_error(f"Failed to create check: {str(e)}")
584
594
  raise typer.Exit(1)
585
595
 
586
- 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):
596
+ 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, regions: Optional[str] = None):
587
597
  """Update an existing check"""
588
598
  try:
589
599
  import json
@@ -629,7 +639,14 @@ class ChecksCommand(BaseCommand):
629
639
  self.display_error(f"Failed to read Playwright script file: {str(e)}")
630
640
  raise typer.Exit(1)
631
641
 
632
- # Parse parameters JSON if provided
642
+ # Handle regions parameter - add to params_dict if provided and not in parameters
643
+ if regions is not None and parameters is None:
644
+ # Parse comma-separated regions into a list
645
+ regions_list = [r.strip() for r in regions.split(',') if r.strip()]
646
+ if regions_list:
647
+ params_dict["regions"] = regions_list
648
+
649
+ # Parse parameters JSON if provided (this takes precedence over --regions)
633
650
  if parameters is not None:
634
651
  try:
635
652
  parsed_params = json.loads(parameters)
@@ -749,7 +766,7 @@ class ChecksCommand(BaseCommand):
749
766
  # If result_id was specified, we're fetching a single result - display it with full formatting
750
767
  if result_id and len(response.results) == 1:
751
768
  result = response.results[0]
752
-
769
+
753
770
  if self.output_format in ['json', 'yaml']:
754
771
  result_dict = {
755
772
  "id": str(result.id) if hasattr(result, 'id') and result.id else None,
@@ -901,10 +918,8 @@ class ChecksCommand(BaseCommand):
901
918
 
902
919
  # Provide helpful hints based on context
903
920
  hints = []
904
- if check_id:
905
- hints.append(f"Use 'pngr checks result {check_id} <result-id>' for detailed result info")
906
- else:
907
- hints.append("Use 'pngr checks results --result-id <result-id>' for detailed result info")
921
+ hints.append("Use 'pngr checks result <result-id>' for detailed result info")
922
+ if not check_id:
908
923
  hints.append("Use 'pngr checks list' to see all available checks and their IDs")
909
924
  hints.append("Filter results: --type <type>, --status <status>, --region <region>")
910
925
 
@@ -935,16 +950,19 @@ class ChecksCommand(BaseCommand):
935
950
  self.display_error(f"Failed to get result details: {str(e)}")
936
951
  raise typer.Exit(1)
937
952
 
938
- def get_check_result_detailed(self, check_id: str, result_id: str, verbose: bool = False):
939
- """Get detailed information for a specific check result using check_id and result_id"""
953
+ def get_check_result_detailed(self, result_id: str, verbose: bool = False):
954
+ """Get detailed information for a specific check result using result_id"""
940
955
  try:
941
- checks_api = self.get_client()
956
+ unified_api = self.get_unified_results_client()
942
957
 
943
- # Use the actual SDK method
944
- result = checks_api.v1_checks_check_id_results_check_result_id_get(
945
- check_id=check_id,
946
- check_result_id=result_id
947
- )
958
+ # Use the unified results API with result_id filter
959
+ response = unified_api.v1_checks_all_results_get(result_id=result_id)
960
+
961
+ if not hasattr(response, 'results') or not response.results or len(response.results) == 0:
962
+ self.display_error(f"Result {result_id} not found")
963
+ raise typer.Exit(1)
964
+
965
+ result = response.results[0]
948
966
 
949
967
  if self.output_format in ['json', 'yaml']:
950
968
  # Full result data for JSON/YAML
@@ -979,7 +997,7 @@ class ChecksCommand(BaseCommand):
979
997
  response_time_display = f"{result.response_time}ms"
980
998
  else:
981
999
  response_time_display = 'N/A'
982
-
1000
+
983
1001
  basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
984
1002
  • Result ID: [white]{result.id}[/white]
985
1003
  • Check ID: [white]{result.check_id}[/white]
@@ -1058,6 +1076,7 @@ class ChecksCommand(BaseCommand):
1058
1076
  region_dict = {
1059
1077
  "id": str(region.id) if hasattr(region, 'id') and region.id else None,
1060
1078
  "display_name": region.display_name if hasattr(region, 'display_name') and region.display_name else None,
1079
+ "aliases": region.aliases if hasattr(region, 'aliases') and region.aliases else None,
1061
1080
  "available_check_types": region.available_check_types if hasattr(region, 'available_check_types') and region.available_check_types else None
1062
1081
  }
1063
1082
  regions_data.append(region_dict)
@@ -1073,6 +1092,7 @@ class ChecksCommand(BaseCommand):
1073
1092
  table = Table(title=table_title)
1074
1093
  table.add_column("Region ID", style="cyan", min_width=15)
1075
1094
  table.add_column("Display Name", style="green", min_width=25)
1095
+ table.add_column("Aliases", style="yellow", min_width=20)
1076
1096
  table.add_column("Available Check Types", style="white")
1077
1097
 
1078
1098
  for region in response.regions:
@@ -1080,6 +1100,14 @@ class ChecksCommand(BaseCommand):
1080
1100
  region_id = str(region.id) if hasattr(region, 'id') and region.id else "-"
1081
1101
  display_name = region.display_name if hasattr(region, 'display_name') and region.display_name else "-"
1082
1102
 
1103
+ # Handle aliases
1104
+ aliases_display = "-"
1105
+ if hasattr(region, 'aliases') and region.aliases:
1106
+ if isinstance(region.aliases, list):
1107
+ aliases_display = ", ".join(region.aliases)
1108
+ else:
1109
+ aliases_display = str(region.aliases)
1110
+
1083
1111
  # Handle available check types
1084
1112
  available_types = "-"
1085
1113
  if hasattr(region, 'available_check_types') and region.available_check_types:
@@ -1091,6 +1119,7 @@ class ChecksCommand(BaseCommand):
1091
1119
  table.add_row(
1092
1120
  region_id,
1093
1121
  display_name,
1122
+ aliases_display,
1094
1123
  available_types
1095
1124
  )
1096
1125
 
@@ -1169,15 +1198,15 @@ def list_checks(
1169
1198
  ):
1170
1199
  """List monitoring checks with advanced filtering options"""
1171
1200
  from ..utils.config import get_output_format
1172
-
1201
+
1173
1202
  # Validate name length
1174
1203
  if name and len(name) > 100:
1175
1204
  typer.echo("Error: Name filter cannot exceed 100 characters", err=True)
1176
1205
  raise typer.Exit(1)
1177
1206
 
1178
1207
  # Validate check type
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)
1208
+ if check_type and check_type not in SUPPORTED_CHECK_TYPES:
1209
+ typer.echo(f"Error: Invalid check type '{check_type}'. Must be one of: {', '.join(SUPPORTED_CHECK_TYPES)}", err=True)
1181
1210
  raise typer.Exit(1)
1182
1211
 
1183
1212
  # Validate status values if provided
@@ -1214,7 +1243,8 @@ def create_check(
1214
1243
  timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
1215
1244
  parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
1216
1245
  pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
1217
- from_file: Optional[str] = typer.Option(None, "--from-file", "-f", help="Path to JSON or YAML file containing check configuration")
1246
+ from_file: Optional[str] = typer.Option(None, "--from-file", "-f", help="Path to JSON or YAML file containing check configuration"),
1247
+ regions: Optional[str] = typer.Option(None, "--regions", help="Comma-separated list of regions (e.g., 'ru-central1,eu-west1')"),
1218
1248
  ):
1219
1249
  """Create a new monitoring check. Can be created from command line options or from a JSON/YAML file.
1220
1250
 
@@ -1236,8 +1266,13 @@ def create_check(
1236
1266
  typer.echo("Error: --name is required when not using --from-file", err=True)
1237
1267
  raise typer.Exit(1)
1238
1268
 
1269
+ # Validate check type
1270
+ if check_type not in SUPPORTED_CHECK_TYPES:
1271
+ typer.echo(f"Error: Invalid check type '{check_type}'. Must be one of: {', '.join(SUPPORTED_CHECK_TYPES)}", err=True)
1272
+ raise typer.Exit(1)
1273
+
1239
1274
  checks_cmd = ChecksCommand(get_output_format())
1240
- checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file, from_file)
1275
+ checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file, from_file, regions)
1241
1276
 
1242
1277
 
1243
1278
  @app.command("update")
@@ -1251,12 +1286,13 @@ def update_check(
1251
1286
  timeout: Optional[int] = typer.Option(None, "--timeout", help="New timeout in seconds"),
1252
1287
  active: bool = typer.Option(None, "--active/--inactive", help="Enable or disable the check"),
1253
1288
  parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"pw_script\": \"...\", \"regions\": [\"US\", \"EU\"]}')"),
1254
- pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks")
1289
+ pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
1290
+ regions: Optional[str] = typer.Option(None, "--regions", help="Comma-separated list of regions (e.g., 'ru-central1,eu-west1')"),
1255
1291
  ):
1256
1292
  """Update an existing check. Use --parameters to provide complex parameters like Playwright scripts, regions, etc."""
1257
1293
  from ..utils.config import get_output_format
1258
1294
  checks_cmd = ChecksCommand(get_output_format())
1259
- checks_cmd.update_check(check_id, name, url, host, port, interval, timeout, active, parameters, pw_script_file)
1295
+ checks_cmd.update_check(check_id, name, url, host, port, interval, timeout, active, parameters, pw_script_file, regions)
1260
1296
 
1261
1297
 
1262
1298
  @app.command("delete")
@@ -1284,20 +1320,25 @@ def get_results(
1284
1320
  ):
1285
1321
  """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
1322
  from ..utils.config import get_output_format
1323
+
1324
+ # Validate check type
1325
+ if check_type and check_type not in SUPPORTED_CHECK_TYPES:
1326
+ typer.echo(f"Error: Invalid check type '{check_type}'. Must be one of: {', '.join(SUPPORTED_CHECK_TYPES)}", err=True)
1327
+ raise typer.Exit(1)
1328
+
1287
1329
  checks_cmd = ChecksCommand(get_output_format())
1288
1330
  checks_cmd.get_check_results(check_id, from_date, to_date, page, page_size, status, check_type, region, result_id)
1289
1331
 
1290
1332
 
1291
1333
  @app.command("result")
1292
1334
  def get_result(
1293
- check_id: str = typer.Argument(..., help="Check ID"),
1294
1335
  result_id: str = typer.Argument(..., help="Result ID to retrieve detailed information for"),
1295
1336
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full detailed information including network requests"),
1296
1337
  ):
1297
1338
  """Get detailed information for a specific check result"""
1298
1339
  from ..utils.config import get_output_format
1299
1340
  checks_cmd = ChecksCommand(get_output_format())
1300
- checks_cmd.get_check_result_detailed(check_id, result_id, verbose)
1341
+ checks_cmd.get_check_result_detailed(result_id, verbose)
1301
1342
 
1302
1343
 
1303
1344
  @app.command("list-regions")
@@ -1306,6 +1347,12 @@ def list_regions(
1306
1347
  ):
1307
1348
  """List available regions for monitoring checks"""
1308
1349
  from ..utils.config import get_output_format
1350
+
1351
+ # Validate check type
1352
+ if check_type and check_type not in SUPPORTED_CHECK_TYPES:
1353
+ typer.echo(f"Error: Invalid check type '{check_type}'. Must be one of: {', '.join(SUPPORTED_CHECK_TYPES)}", err=True)
1354
+ raise typer.Exit(1)
1355
+
1309
1356
  checks_cmd = ChecksCommand(get_output_format())
1310
1357
  checks_cmd.list_regions(check_type)
1311
1358
 
@@ -13,14 +13,18 @@ from rich.panel import Panel
13
13
  from .base import BaseCommand
14
14
  from ..utils.config import get_api_key
15
15
 
16
+ # Supported check types for on-demand execution
17
+ SUPPORTED_CHECK_TYPES = ["web", "api", "tcp", "ssl", "dns", "icmp", "portscan", "synthetic", "multistep"]
18
+
16
19
 
17
20
  class OnDemandChecksCommand(BaseCommand):
18
21
  """
19
22
  Commands for managing on-demand monitoring checks
20
23
  """
21
24
 
22
- def __init__(self, output_format: Optional[str] = None):
25
+ def __init__(self, output_format: Optional[str] = None, verbose: bool = False):
23
26
  super().__init__(output_format)
27
+ self.verbose = verbose
24
28
 
25
29
  def _parse_check_file(self, file_path: str) -> dict:
26
30
  """Parse check configuration from JSON or YAML file (local or URL)"""
@@ -63,7 +67,7 @@ class OnDemandChecksCommand(BaseCommand):
63
67
  self.display_error(f"Failed to initialize client: {str(e)}")
64
68
  raise typer.Exit(1)
65
69
 
66
- 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, from_file: Optional[str] = None, wait_for_result: bool = True):
70
+ def execute_custom_check(self, url: Optional[str] = None, check_type: str = "web", host: Optional[str] = None, port: Optional[int] = None, timeout: Optional[int] = None, name: str = "On-demand check", regions: Optional[str] = None, parameters: Optional[str] = None, pw_script_file: Optional[str] = None, from_file: Optional[str] = None, wait_for_result: bool = True, ports: Optional[str] = None):
67
71
  """Execute custom on-demand check"""
68
72
  try:
69
73
  import json
@@ -85,10 +89,15 @@ class OnDemandChecksCommand(BaseCommand):
85
89
  # Command line options take precedence over file data
86
90
  check_data = {
87
91
  "name": filtered_file_data.get("name", name),
88
- "type": filtered_file_data.get("type", check_type),
89
- "timeout": filtered_file_data.get("timeout", timeout)
92
+ "type": filtered_file_data.get("type", check_type)
90
93
  }
91
94
 
95
+ # Handle timeout - only set if explicitly provided or in file
96
+ if timeout is not None:
97
+ check_data["timeout"] = timeout
98
+ elif "timeout" in filtered_file_data:
99
+ check_data["timeout"] = filtered_file_data["timeout"]
100
+
92
101
  # Override with command line values if provided
93
102
  if name != "On-demand check": # Only override if name was explicitly provided
94
103
  check_data["name"] = name
@@ -141,9 +150,12 @@ class OnDemandChecksCommand(BaseCommand):
141
150
  # Build check data normally
142
151
  check_data = {
143
152
  "name": name,
144
- "type": check_type,
145
- "timeout": timeout
153
+ "type": check_type
146
154
  }
155
+
156
+ # Only set timeout if explicitly provided by user
157
+ if timeout is not None:
158
+ check_data["timeout"] = timeout
147
159
 
148
160
  # Add URL if provided (required for web, api, ssl checks)
149
161
  if url is not None:
@@ -177,7 +189,18 @@ class OnDemandChecksCommand(BaseCommand):
177
189
  self.display_error(f"Failed to read Playwright script file: {str(e)}")
178
190
  raise typer.Exit(1)
179
191
 
180
- # Parse parameters JSON if provided
192
+ # Handle regions parameter - add to params_dict if provided and not in parameters
193
+ if regions is not None and parameters is None:
194
+ # Parse comma-separated regions into a list
195
+ regions_list = [r.strip() for r in regions.split(',') if r.strip()]
196
+ if regions_list:
197
+ params_dict["regions"] = regions_list
198
+
199
+ # Handle ports parameter - add to params_dict if provided and not in parameters
200
+ if ports is not None and parameters is None:
201
+ params_dict["ports"] = ports
202
+
203
+ # Parse parameters JSON if provided (this takes precedence over --regions and --ports)
181
204
  if parameters is not None:
182
205
  try:
183
206
  parsed_params = json.loads(parameters)
@@ -194,11 +217,17 @@ class OnDemandChecksCommand(BaseCommand):
194
217
 
195
218
  # Validation based on check type - use the actual type from check_data
196
219
  actual_check_type = check_data.get("type", check_type)
220
+
221
+ # Validate check type is supported
222
+ if actual_check_type not in SUPPORTED_CHECK_TYPES:
223
+ self.display_error(f"Invalid check type: {actual_check_type}. Supported types: {', '.join(SUPPORTED_CHECK_TYPES)}")
224
+ raise typer.Exit(1)
225
+
197
226
  if actual_check_type in ['web', 'api'] and not url:
198
227
  self.display_error(f"URL is required for {actual_check_type} checks")
199
228
  raise typer.Exit(1)
200
229
 
201
- if actual_check_type in ['tcp'] and not host:
230
+ if actual_check_type in ['tcp', 'portscan', 'dns', 'icmp'] and not host:
202
231
  self.display_error(f"Host is required for {actual_check_type} checks")
203
232
  raise typer.Exit(1)
204
233
 
@@ -446,15 +475,17 @@ class OnDemandChecksCommand(BaseCommand):
446
475
  }
447
476
  self.output_data(job_data)
448
477
  else:
449
- # Rich formatted detailed view
450
- self._display_detailed_job_status(job_status, job_id)
478
+ # Rich formatted detailed view - fetch and display with results
479
+ self._fetch_and_display_job_result(job_id, job_status, verbose=self.verbose)
451
480
 
452
481
  except Exception as e:
453
482
  self.display_error(f"Failed to get job status: {str(e)}")
454
483
  raise typer.Exit(1)
455
484
 
456
- def _fetch_and_display_job_result(self, job_id: str, job_status):
485
+ def _fetch_and_display_job_result(self, job_id: str, job_status, verbose: bool = False):
457
486
  """Fetch detailed results from the new results endpoint and display them"""
487
+ import time
488
+
458
489
  try:
459
490
  # Extract result_id(s) from job response
460
491
  result_ids = []
@@ -463,14 +494,56 @@ class OnDemandChecksCommand(BaseCommand):
463
494
  if hasattr(job_status, 'result') and job_status.result:
464
495
  result = job_status.result
465
496
 
497
+ # Debug: Print the raw result to see what we're getting (only in verbose mode)
498
+ if verbose and self.output_format not in ['json', 'yaml']:
499
+ import json
500
+ self.console.print(f"[dim]DEBUG: Raw result from job status:[/dim]")
501
+ self.console.print(f"[dim]{json.dumps(result, indent=2, default=str)}[/dim]")
502
+
466
503
  # Check if this is a multi-region result
467
504
  if isinstance(result, dict):
468
505
  if 'regional_summary' in result and result['regional_summary']:
469
506
  # Multi-region execution
470
507
  is_multi_region = True
471
- for regional_result in result['regional_summary']:
508
+ regional_summary = result['regional_summary']
509
+
510
+ # Debug: Show what we found (only in verbose mode)
511
+ if verbose and self.output_format not in ['json', 'yaml']:
512
+ self.console.print(f"[dim]DEBUG: Found {len(regional_summary)} regions in regional_summary[/dim]")
513
+
514
+ for regional_result in regional_summary:
472
515
  if 'result_id' in regional_result:
473
516
  result_ids.append(regional_result['result_id'])
517
+
518
+ # Check if we might be missing results (race condition detection)
519
+ total_regions = result.get('total_regions', len(regional_summary))
520
+ completed_regions = result.get('completed_regions', len(regional_summary))
521
+
522
+ if verbose and self.output_format not in ['json', 'yaml']:
523
+ self.console.print(f"[dim]DEBUG: total_regions={total_regions}, completed_regions={completed_regions}, found result_ids={len(result_ids)}[/dim]")
524
+
525
+ # If we're missing results, wait a bit and refetch
526
+ if len(result_ids) < total_regions:
527
+ if self.output_format not in ['json', 'yaml']:
528
+ self.console.print(f"[yellow]⚠ Race condition detected: {len(result_ids)}/{total_regions} results ready. Waiting 2s and retrying...[/yellow]")
529
+ time.sleep(2)
530
+
531
+ # Refetch job status
532
+ checks_api = self.get_client()
533
+ job_status = checks_api.v1_checks_jobs_job_id_get(job_id=job_id)
534
+
535
+ # Re-extract result_ids
536
+ result_ids = []
537
+ if hasattr(job_status, 'result') and job_status.result:
538
+ result = job_status.result
539
+ if isinstance(result, dict) and 'regional_summary' in result:
540
+ for regional_result in result['regional_summary']:
541
+ if 'result_id' in regional_result:
542
+ result_ids.append(regional_result['result_id'])
543
+
544
+ if verbose and self.output_format not in ['json', 'yaml']:
545
+ self.console.print(f"[dim]DEBUG: After retry, found {len(result_ids)} result_ids[/dim]")
546
+
474
547
  elif 'result_id' in result:
475
548
  # Single-region execution
476
549
  result_ids.append(result['result_id'])
@@ -487,14 +560,14 @@ class OnDemandChecksCommand(BaseCommand):
487
560
  raise typer.Exit(1)
488
561
 
489
562
  from pingera import ApiClient, Configuration
490
- from pingera.api import ChecksApi
563
+ from pingera.api import ChecksUnifiedResultsApi
491
564
  from ..utils.config import get_config
492
565
 
493
566
  configuration = Configuration()
494
567
  configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
495
568
  configuration.api_key['apiKeyAuth'] = api_key
496
569
  api_client = ApiClient(configuration)
497
- checks_api = ChecksApi(api_client)
570
+ checks_api = ChecksUnifiedResultsApi(api_client)
498
571
 
499
572
  # Fetch detailed results for each result_id
500
573
  if is_multi_region:
@@ -547,13 +620,15 @@ class OnDemandChecksCommand(BaseCommand):
547
620
  self.console.print(f"\n[bold cyan]Multi-Region Execution Result[/bold cyan]")
548
621
  self.console.print(f"• Job ID: [white]{job_id}[/white]")
549
622
  self.console.print(f"• Status: [{status_color}]{status_emoji} {job_status.status}[/{status_color}]")
550
- self.console.print(f"• Regions: [white]{len(result_ids)}[/white]")
551
623
 
552
624
  # Extract and display aggregated statistics
553
625
  if hasattr(job_status, 'result') and isinstance(job_status.result, dict):
554
626
  result = job_status.result
555
627
  regional_summary = result.get('regional_summary', [])
556
628
 
629
+ # Display region count from actual regional_summary
630
+ self.console.print(f"• Regions: [white]{len(regional_summary)}[/white]")
631
+
557
632
  if regional_summary:
558
633
  # Calculate aggregated statistics
559
634
  response_times = []
@@ -616,19 +691,68 @@ class OnDemandChecksCommand(BaseCommand):
616
691
 
617
692
  def _display_detailed_result_with_job_info(self, job_status, job_id: str, detailed_result):
618
693
  """Display detailed result information combined with job info"""
619
- # Use the existing detailed result display logic from checks.py
620
- # For now, use the job status display and note about detailed results
621
- self._display_detailed_job_status(job_status, job_id)
694
+ from rich.panel import Panel
695
+ from ..formatters.registry import FormatterRegistry
696
+
697
+ # Determine status color and emoji
698
+ status_emoji = "✅" if hasattr(detailed_result, 'status') and detailed_result.status == 'ok' else "❌"
699
+ status_color = "green" if hasattr(detailed_result, 'status') and detailed_result.status == 'ok' else "red"
622
700
 
623
- # Additional result details if available
701
+ # Basic information
702
+ basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
703
+ • Result ID: [white]{detailed_result.id if hasattr(detailed_result, 'id') else 'Unknown'}[/white]
704
+ • Check ID: [white]{detailed_result.check_id if hasattr(detailed_result, 'check_id') and detailed_result.check_id else 'None'}[/white]
705
+ • Status: [{status_color}]{status_emoji} {detailed_result.status if hasattr(detailed_result, 'status') else 'Unknown'}[/{status_color}]
706
+ • Timestamp: [white]{detailed_result.created_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(detailed_result, 'created_at') and detailed_result.created_at else 'Unknown'}[/white]"""
707
+
708
+ if hasattr(detailed_result, 'response_time') and detailed_result.response_time:
709
+ basic_info += f"\n• Response Time: [yellow]{detailed_result.response_time}ms[/yellow]"
710
+
711
+ # Server information
712
+ if hasattr(detailed_result, 'check_server') and detailed_result.check_server:
713
+ server = detailed_result.check_server
714
+ server_info = f"""• Check Server: [white]{server.id if hasattr(server, 'id') else 'Unknown'}[/white]
715
+ • Server IP: [white]{server.ip_address if hasattr(server, 'ip_address') else 'Unknown'}[/white]
716
+ • Server Country: [white]{server.country if hasattr(server, 'country') else 'Unknown'}[/white]
717
+ • Server Region: [white]{server.region if hasattr(server, 'region') else 'Unknown'}[/white]"""
718
+ basic_info += f"\n{server_info}"
719
+
720
+ # Error information
721
+ error_info = ""
722
+ if hasattr(detailed_result, 'error_message') and detailed_result.error_message:
723
+ error_info = f"""
724
+ [bold red]Error Information:[/bold red]
725
+ • Error: [red]{detailed_result.error_message}[/red]"""
726
+
727
+ # Metadata analysis using formatters
728
+ metadata_info = ""
624
729
  if hasattr(detailed_result, 'check_metadata') and detailed_result.check_metadata:
625
- from ..formatters.registry import FormatterRegistry
626
- registry = FormatterRegistry(verbose=False)
627
- metadata_formatted = registry.format_metadata(detailed_result.check_metadata)
730
+ metadata = detailed_result.check_metadata
628
731
 
629
- if metadata_formatted.strip():
630
- self.console.print(f"\n[bold cyan]Detailed Result Metadata:[/bold cyan]")
631
- self.console.print(metadata_formatted)
732
+ if isinstance(metadata, dict):
733
+ # Add result_id to metadata for formatters to use in truncation notice
734
+ metadata_with_id = metadata.copy()
735
+ if hasattr(detailed_result, 'id'):
736
+ metadata_with_id['result_id'] = detailed_result.id
737
+
738
+ registry = FormatterRegistry(verbose=self.verbose)
739
+ metadata_info = registry.format_metadata(metadata_with_id)
740
+
741
+ # Combine all sections
742
+ full_info = basic_info
743
+ if error_info:
744
+ full_info += error_info
745
+ if metadata_info:
746
+ full_info += metadata_info
747
+
748
+ panel = Panel(
749
+ full_info,
750
+ title=f"🔍 Result Details: {detailed_result.id if hasattr(detailed_result, 'id') else 'Unknown'}",
751
+ border_style="blue",
752
+ padding=(1, 2),
753
+ )
754
+
755
+ self.console.print(panel)
632
756
 
633
757
  def _display_detailed_job_status(self, job_status, job_id: str):
634
758
  """Display detailed job status information in a rich format"""
@@ -806,7 +930,7 @@ class OnDemandChecksCommand(BaseCommand):
806
930
  time.sleep(0.5) # Brief pause to show completion
807
931
 
808
932
  # Fetch and display the actual result
809
- self._fetch_and_display_job_result(job_id, job_status)
933
+ self._fetch_and_display_job_result(job_id, job_status, verbose=self.verbose)
810
934
  return
811
935
 
812
936
  elif job_status.status == 'running':
@@ -858,14 +982,22 @@ def get_output_format():
858
982
  return get_config().get('output_format', 'table')
859
983
 
860
984
 
985
+ def get_verbose_mode():
986
+ """Get verbose mode from config"""
987
+ from ..utils.config import get_verbose_mode as _get_verbose_mode
988
+ return _get_verbose_mode()
989
+
990
+
861
991
  @run_app.command("custom")
862
992
  def run_custom_check(
863
993
  url: Optional[str] = typer.Option(None, "--url", "-u", help="URL to monitor (required for web, api, ssl checks)"),
864
- check_type: str = typer.Option("web", "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
865
- host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL checks (max 255 characters)"),
866
- port: Optional[int] = typer.Option(None, "--port", help="Port number for TCP checks (1-65535)"),
867
- timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
994
+ check_type: str = typer.Option("web", "--type", "-t", help=f"Check type: {', '.join(SUPPORTED_CHECK_TYPES)}"),
995
+ host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL/portscan/DNS/ICMP checks (max 255 characters)"),
996
+ port: Optional[int] = typer.Option(None, "--port", "-p", help="Port number for TCP/SSL checks (1-65535)"),
997
+ ports: Optional[str] = typer.Option(None, "--ports", help="Ports to scan for portscan checks (e.g., '80,443' or '1-1024'). Overridden by --parameters if both are specified."),
998
+ timeout: Optional[int] = typer.Option(None, "--timeout", help="Timeout in seconds (optional - backend will use defaults if not specified)"),
868
999
  name: str = typer.Option("On-demand check", "--name", "-n", help="Check name"),
1000
+ regions: Optional[str] = typer.Option(None, "--regions", "-r", help="Comma-separated list of regions (e.g., 'ru-central1,eu-west1'). Overridden by --parameters if both are specified."),
869
1001
  parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
870
1002
  pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
871
1003
  from_file: Optional[str] = typer.Option(None, "--from-file", "-f", help="Path to JSON or YAML file containing check configuration"),
@@ -883,10 +1015,13 @@ def run_custom_check(
883
1015
  Parameters vary by check type:
884
1016
  - web/api: --url required
885
1017
  - tcp: --host required, --port optional
886
- - ssl: --url or --host required
887
- - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
888
- on_demand_cmd = OnDemandChecksCommand(get_output_format())
889
- on_demand_cmd.execute_custom_check(url, check_type, host, port, timeout, name, parameters, pw_script_file, from_file, not no_wait)
1018
+ - ssl: --url or --host required, --port optional
1019
+ - portscan: --host required, --ports optional
1020
+ - synthetic/multistep: --pw-script-file or --parameters with pw_script required
1021
+
1022
+ Timeout is optional - if not specified, backend will use appropriate defaults for each check type."""
1023
+ on_demand_cmd = OnDemandChecksCommand(get_output_format(), verbose=get_verbose_mode())
1024
+ on_demand_cmd.execute_custom_check(url, check_type, host, port, timeout, name, regions, parameters, pw_script_file, from_file, not no_wait, ports)
890
1025
 
891
1026
 
892
1027
  @run_app.command("existing")
@@ -895,7 +1030,7 @@ def run_existing_check(
895
1030
  no_wait: bool = typer.Option(False, "--no-wait", help="Don't wait for job completion, just queue the check and return job ID"),
896
1031
  ):
897
1032
  """Execute existing check on demand. By default, waits for job completion and shows result immediately (max 5 minutes). Use --no-wait to just queue the check."""
898
- on_demand_cmd = OnDemandChecksCommand(get_output_format())
1033
+ on_demand_cmd = OnDemandChecksCommand(get_output_format(), verbose=get_verbose_mode())
899
1034
  on_demand_cmd.execute_existing_check(check_id, not no_wait)
900
1035
 
901
1036
 
@@ -905,7 +1040,7 @@ def list_jobs(
905
1040
  page_size: int = typer.Option(20, "--page-size", "-s", help="Items per page"),
906
1041
  ):
907
1042
  """List check jobs"""
908
- on_demand_cmd = OnDemandChecksCommand(get_output_format())
1043
+ on_demand_cmd = OnDemandChecksCommand(get_output_format(), verbose=get_verbose_mode())
909
1044
  on_demand_cmd.list_jobs(page, page_size)
910
1045
 
911
1046
 
@@ -914,5 +1049,5 @@ def get_job_status(
914
1049
  job_id: str = typer.Argument(..., help="Job ID to get status for"),
915
1050
  ):
916
1051
  """Get job status and results. For multi-region executions, shows aggregated statistics and regional summary."""
917
- on_demand_cmd = OnDemandChecksCommand(get_output_format())
1052
+ on_demand_cmd = OnDemandChecksCommand(get_output_format(), verbose=get_verbose_mode())
918
1053
  on_demand_cmd.get_job_status(job_id)