pingera-cli 0.1.5__tar.gz → 0.1.7__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 (49) hide show
  1. {pingera_cli-0.1.5/pingera_cli.egg-info → pingera_cli-0.1.7}/PKG-INFO +1 -1
  2. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/docs/cli-commands.md +93 -7
  3. pingera_cli-0.1.7/docs/example-check.yaml +21 -0
  4. pingera_cli-0.1.7/docs/example-synthetic-screenshot.json +21 -0
  5. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/__init__.py +1 -1
  6. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/checks.py +145 -25
  7. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/on_demand_checks.py +129 -18
  8. {pingera_cli-0.1.5 → pingera_cli-0.1.7/pingera_cli.egg-info}/PKG-INFO +1 -1
  9. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/SOURCES.txt +2 -0
  10. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/.gitignore +0 -0
  11. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/.replit +0 -0
  12. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/LICENSE +0 -0
  13. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/README.md +0 -0
  14. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/assert +0 -0
  15. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/__main__.py +0 -0
  16. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/__init__.py +0 -0
  17. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/auth.py +0 -0
  18. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/base.py +0 -0
  19. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/check_groups.py +0 -0
  20. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/check_secrets.py +0 -0
  21. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/secrets.py +0 -0
  22. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/__init__.py +0 -0
  23. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/base_formatter.py +0 -0
  24. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/generic_formatter.py +0 -0
  25. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/multistep_formatter.py +0 -0
  26. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/registry.py +0 -0
  27. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/ssl_formatter.py +0 -0
  28. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/synthetic_formatter.py +0 -0
  29. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/web_formatter.py +0 -0
  30. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/main.py +0 -0
  31. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/utils/__init__.py +0 -0
  32. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/utils/config.py +0 -0
  33. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/utils/console.py +0 -0
  34. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/dependency_links.txt +0 -0
  35. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/entry_points.txt +0 -0
  36. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/requires.txt +0 -0
  37. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/top_level.txt +0 -0
  38. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pyproject.toml +0 -0
  39. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/replit.md +0 -0
  40. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/run_tests.py +0 -0
  41. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/setup.cfg +0 -0
  42. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/setup.py +0 -0
  43. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/__init__.py +0 -0
  44. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/conftest.py +0 -0
  45. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/requirements.txt +0 -0
  46. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_auth.py +0 -0
  47. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_checks.py +0 -0
  48. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_config.py +0 -0
  49. {pingera_cli-0.1.5 → pingera_cli-0.1.7}/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.5
3
+ Version: 0.1.7
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
@@ -9,7 +9,7 @@ The `pngr` CLI provides a comprehensive interface to the Pingera monitoring plat
9
9
  These options are available for all commands:
10
10
 
11
11
  ```
12
- --api-key <key> API key for authentication (can also use PINGERA_API_KEY env var)
12
+ --api-key <key> API key for authentication (can also be used PINGERA_API_KEY env var)
13
13
  --base-url <url> API base URL (default: https://api.pingera.ru)
14
14
  --output <format> Output format: table, json, yaml (default: table)
15
15
  --page-id <id> Default page ID for status page operations (can also use PINGERA_PAGE_ID env var)
@@ -183,7 +183,7 @@ pngr heartbeats get <heartbeat-id>
183
183
  pngr heartbeats create --name <name> --interval <seconds> [--grace-period <seconds>]
184
184
 
185
185
  # Update heartbeat
186
- pngr heartbeats update <heartbeat-id> [--name <name>] [--interval <seconds>]
186
+ pngr heartbeats update <heartbeat-id> --name <name> --interval <seconds>
187
187
 
188
188
  # Delete heartbeat
189
189
  pngr heartbeats delete <heartbeat-id> [--confirm]
@@ -211,7 +211,7 @@ pngr pages get <page-id>
211
211
  pngr pages create --name <name> [--description <desc>] [--subdomain <sub>] [--theme <theme>]
212
212
 
213
213
  # Update status page
214
- pngr pages update <page-id> [--name <name>] [--description <desc>] [--theme <theme>]
214
+ pngr pages update <page-id> --name <name> --description <desc> --theme <theme>
215
215
 
216
216
  # Delete status page
217
217
  pngr pages delete <page-id> [--confirm]
@@ -231,7 +231,7 @@ pngr components get <component-id> [--page-id <id>]
231
231
  pngr components create --name <name> [--description <desc>] [--status <status>] [--page-id <id>]
232
232
 
233
233
  # Update component
234
- pngr components update <component-id> [--name <name>] [--description <desc>] [--page-id <id>]
234
+ pngr components update <component-id> --name <name> --description <desc> --page-id <id>
235
235
 
236
236
  # Delete component
237
237
  pngr components delete <component-id> [--page-id <id>] [--confirm]
@@ -260,7 +260,7 @@ pngr incidents get <incident-id> [--page-id <id>]
260
260
  pngr incidents create --name <name> --body <text> --status <status> --impact <impact> [--page-id <id>]
261
261
 
262
262
  # Update incident
263
- pngr incidents update <incident-id> [--name <name>] [--body <text>] [--status <status>] [--page-id <id>]
263
+ pngr incidents update <incident-id> --name <name> --body <text> --status <status> --page-id <id>
264
264
 
265
265
  # Delete incident
266
266
  pngr incidents delete <incident-id> [--page-id <id>] [--confirm]
@@ -273,7 +273,93 @@ pngr incidents updates update <incident-id> <update-id> --body <text> [--page-id
273
273
  pngr incidents updates delete <incident-id> <update-id> [--page-id <id>] [--confirm]
274
274
  ```
275
275
 
276
- ## Secrets Management
276
+ ## 📄 Check Configuration Files
277
+
278
+ You can create checks from JSON or YAML configuration files using the `--from-file` option:
279
+
280
+ ### JSON Format
281
+ ```json
282
+ {
283
+ "name": "My API Check",
284
+ "type": "api",
285
+ "url": "https://api.example.com/health",
286
+ "interval": 300,
287
+ "timeout": 30,
288
+ "parameters": {
289
+ "regions": ["US", "EU"],
290
+ "http_request": {
291
+ "method": "POST",
292
+ "headers": {
293
+ "Authorization": "Bearer token123",
294
+ "Content-Type": "application/json"
295
+ },
296
+ "body": "{\"check\": \"health\"}"
297
+ }
298
+ }
299
+ }
300
+ ```
301
+
302
+ ### YAML Format
303
+ ```yaml
304
+ name: "My Synthetic Check"
305
+ type: "synthetic"
306
+ interval: 600
307
+ timeout: 60
308
+ parameters:
309
+ regions:
310
+ - "US"
311
+ - "EU"
312
+ pw_script: |
313
+ const { test, expect } = require('@playwright/test');
314
+ test('login test', async ({ page }) => {
315
+ await page.goto('https://example.com/login');
316
+ await page.fill('#username', 'testuser');
317
+ await page.fill('#password', 'password');
318
+ await page.click('button[type="submit"]');
319
+ await expect(page).toHaveURL(/dashboard/);
320
+ });
321
+ # Marketplace fields (ignored by CLI, used by web interface)
322
+ marketplace:
323
+ tags: ["authentication", "login"]
324
+ description: "Tests user login functionality"
325
+ category: "user-flows"
326
+ ```
327
+
328
+ ### Field Priority
329
+ When using `--from-file` with command line options:
330
+ - Command line options override file values
331
+ - File provides defaults for unspecified options
332
+ - Required validations still apply
333
+ - Non-SDK fields (like `marketplace`, custom fields) are ignored and not sent to the API
334
+ - CLI-specific fields like `pw_script_file` are processed but not sent to the SDK
335
+
336
+ ### Create a new check
337
+ ```bash
338
+ # From command line options
339
+ pngr checks create --name "My Website" --type web --url "https://example.com"
340
+
341
+ # From configuration file
342
+ pngr checks create --from-file check-config.json
343
+ pngr checks create --from-file check-config.yaml
344
+
345
+ # Combine file and command line (command line options override file values)
346
+ pngr checks create --from-file check-config.json --name "Override Name"
347
+ ```
348
+
349
+ ### Run on-demand checks
350
+ ```bash
351
+ # From command line options
352
+ pngr checks run custom --url https://example.com --type web --wait-for-result
353
+
354
+ # From configuration file
355
+ pngr checks run custom --from-file check-config.json --wait-for-result
356
+ pngr checks run custom --from-file check-config.yaml --wait-for-result
357
+
358
+ # Combine file and command line (command line options override file values)
359
+ pngr checks run custom --from-file check-config.json --name "Override Name" --wait-for-result
360
+ ```
361
+
362
+ ## 🔐 Secrets Management
277
363
 
278
364
  Manage organization secrets for use in monitoring checks:
279
365
 
@@ -400,7 +486,7 @@ pngr config set defaults.page_id your-page-id
400
486
  pngr checks list --status active
401
487
 
402
488
  # Create a web check
403
- pngr checks create --name "API Health" --type web --url https://api.example.com --interval 300
489
+ pngr checks create --name "My Website" --type web --url "https://example.com"
404
490
 
405
491
  # Create a TCP check
406
492
  pngr checks create --name "Database Connection" --type tcp --host db.example.com --port 5432
@@ -0,0 +1,21 @@
1
+
2
+ name: "Example Synthetic Check"
3
+ type: "synthetic"
4
+ interval: 600
5
+ timeout: 60
6
+ parameters:
7
+ regions:
8
+ - "US"
9
+ - "EU"
10
+ pw_script: |
11
+ const { test, expect } = require('@playwright/test');
12
+
13
+ test('example test', async ({ page }) => {
14
+ await page.goto('https://example.com');
15
+ await expect(page).toHaveTitle(/Example/);
16
+ await page.click('text=Login');
17
+ await page.fill('input[name="username"]', 'testuser');
18
+ await page.fill('input[name="password"]', 'testpass');
19
+ await page.click('button[type="submit"]');
20
+ await expect(page).toHaveURL(/dashboard/);
21
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "Website Screenshot Monitor",
3
+ "type": "synthetic",
4
+ "interval": 1800,
5
+ "timeout": 30,
6
+ "parameters": {
7
+ "regions": [
8
+ "RU, Moscow"
9
+ ],
10
+ "pw_script": "const { test, expect } = require('@playwright/test');\n\ntest('website screenshot test', async ({ page }) => {\n // Navigate to the website\n await page.goto('https://pingera.ru');\n \n // Wait for the page to fully load\n await page.waitForLoadState('load');\n \n // Additional wait to ensure all dynamic content is loaded\n await page.waitForTimeout(2000);\n \n // Take a full page screenshot\n await page.screenshot({ \n path: 'website-screenshot.png', \n fullPage: true \n });\n \n // Verify the page loaded successfully\n await expect(page).toHaveTitle(/Pingera - платформа мониторинга/);\n \n // Check for key elements on the page\n await expect(page.locator('h1')).toBeVisible();\n \n console.log('Screenshot captured and page verified successfully');\n});"
11
+ },
12
+ "marketplace": {
13
+ "tags": [
14
+ "screenshot",
15
+ "visual-monitoring",
16
+ "website-monitoring"
17
+ ],
18
+ "name": "Website Screenshot Monitor",
19
+ "description": "Captures full-page screenshots of websites and verifies page loading"
20
+ }
21
+ }
@@ -2,7 +2,7 @@
2
2
  Pingera CLI - A beautiful CLI tool for Pingera Platform
3
3
  """
4
4
 
5
- __version__ = "0.1.5"
5
+ __version__ = "0.1.7"
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
 
@@ -389,20 +389,124 @@ class ChecksCommand(BaseCommand):
389
389
  self.display_error(f"Failed to get check: {str(e)}")
390
390
  raise typer.Exit(1)
391
391
 
392
- 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):
392
+ def _parse_check_file(self, file_path: str) -> dict:
393
+ """Parse check configuration from JSON or YAML file"""
394
+ import json
395
+ import os
396
+
397
+ if not os.path.exists(file_path):
398
+ self.display_error(f"Check file not found: {file_path}")
399
+ raise typer.Exit(1)
400
+
401
+ try:
402
+ with open(file_path, 'r', encoding='utf-8') as f:
403
+ content = f.read().strip()
404
+
405
+ if not content:
406
+ self.display_error(f"Check file is empty: {file_path}")
407
+ raise typer.Exit(1)
408
+
409
+ # Try to determine format from file extension or content
410
+ file_ext = os.path.splitext(file_path)[1].lower()
411
+
412
+ if file_ext in ['.yaml', '.yml']:
413
+ try:
414
+ import yaml
415
+ return yaml.safe_load(content)
416
+ except ImportError:
417
+ self.display_error("YAML support not available. Install with: pip install pyyaml")
418
+ raise typer.Exit(1)
419
+ except yaml.YAMLError as e:
420
+ self.display_error(f"Invalid YAML in file {file_path}: {str(e)}")
421
+ raise typer.Exit(1)
422
+ else:
423
+ # Default to JSON
424
+ try:
425
+ return json.loads(content)
426
+ except json.JSONDecodeError as e:
427
+ self.display_error(f"Invalid JSON in file {file_path}: {str(e)}")
428
+ raise typer.Exit(1)
429
+
430
+ except IOError as e:
431
+ self.display_error(f"Failed to read check file: {str(e)}")
432
+ raise typer.Exit(1)
433
+
434
+ 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):
393
435
  """Create a new monitoring check"""
394
436
  try:
395
437
  import json
396
438
  import os
397
439
  checks_api = self.get_client()
398
440
 
399
- # Build check data
400
- check_data = {
401
- "name": name,
402
- "type": check_type,
403
- "interval": interval,
404
- "timeout": timeout
405
- }
441
+ # If creating from file, parse file and merge with command line options
442
+ if from_file:
443
+ file_data = self._parse_check_file(from_file)
444
+
445
+ # Filter only SDK-recognized fields from file data
446
+ # This ignores marketplace-specific fields and other extensions
447
+ sdk_fields = ["name", "type", "url", "host", "port", "interval", "timeout", "parameters", "active"]
448
+ filtered_file_data = {k: v for k, v in file_data.items() if k in sdk_fields}
449
+
450
+ # Command line options take precedence over file data
451
+ check_data = {
452
+ "name": filtered_file_data.get("name", name),
453
+ "type": filtered_file_data.get("type", check_type),
454
+ "interval": filtered_file_data.get("interval", interval),
455
+ "timeout": filtered_file_data.get("timeout", timeout)
456
+ }
457
+
458
+ # Override with command line values if provided (but only if they were explicitly changed from defaults)
459
+ # We need to be more careful about when to override file values
460
+ # For now, just use file values and let user override with explicit CLI options
461
+ if interval != 300: # Only override if interval was explicitly provided
462
+ check_data["interval"] = interval
463
+ if timeout != 30: # Only override if timeout was explicitly provided
464
+ check_data["timeout"] = timeout
465
+
466
+ # Handle URL from file
467
+ if url is None and filtered_file_data.get("url"):
468
+ url = filtered_file_data["url"]
469
+ elif url is not None:
470
+ # Command line URL takes precedence
471
+ pass
472
+
473
+ # Handle host/port from file
474
+ if host is None and filtered_file_data.get("host"):
475
+ host = filtered_file_data["host"]
476
+ elif host is not None:
477
+ # Command line host takes precedence
478
+ pass
479
+
480
+ if port is None and filtered_file_data.get("port"):
481
+ port = filtered_file_data["port"]
482
+ elif port is not None:
483
+ # Command line port takes precedence
484
+ pass
485
+
486
+ # Handle parameters from file
487
+ file_parameters = filtered_file_data.get("parameters")
488
+ if file_parameters and parameters is None:
489
+ # Use file parameters if no command line parameters
490
+ parameters = json.dumps(file_parameters) if isinstance(file_parameters, dict) else file_parameters
491
+
492
+ # Handle pw_script_file from file (this is CLI-specific, not sent to SDK)
493
+ if pw_script_file is None and file_data.get("pw_script_file"):
494
+ pw_script_file = file_data["pw_script_file"]
495
+
496
+ # Show info about ignored fields if any
497
+ ignored_fields = [k for k in file_data.keys() if k not in sdk_fields and k != "pw_script_file"]
498
+ if ignored_fields:
499
+ self.display_info(f"Ignoring non-SDK fields from file: {', '.join(ignored_fields)}")
500
+
501
+ self.display_info(f"Creating check from file: {from_file}")
502
+ else:
503
+ # Build check data normally
504
+ check_data = {
505
+ "name": name,
506
+ "type": check_type,
507
+ "interval": interval,
508
+ "timeout": timeout
509
+ }
406
510
 
407
511
  # Add URL if provided (required for web, api, ssl checks)
408
512
  if url is not None:
@@ -451,28 +555,31 @@ class ChecksCommand(BaseCommand):
451
555
  if params_dict:
452
556
  check_data["parameters"] = params_dict
453
557
 
454
- # Validation based on check type
455
- if check_type in ['web', 'api'] and not url:
456
- self.display_error(f"URL is required for {check_type} checks")
558
+ # Validation based on check type - use the actual type from check_data
559
+ actual_check_type = check_data.get("type", check_type)
560
+ if actual_check_type in ['web', 'api'] and not url:
561
+ self.display_error(f"URL is required for {actual_check_type} checks")
457
562
  raise typer.Exit(1)
458
563
 
459
- if check_type in ['tcp'] and not host:
460
- self.display_error(f"Host is required for {check_type} checks")
564
+ if actual_check_type in ['tcp'] and not host:
565
+ self.display_error(f"Host is required for {actual_check_type} checks")
461
566
  raise typer.Exit(1)
462
567
 
463
- if check_type in ['ssl'] and not (url or host):
464
- self.display_error(f"Either URL or host is required for {check_type} checks")
568
+ if actual_check_type in ['ssl'] and not (url or host):
569
+ self.display_error(f"Either URL or host is required for {actual_check_type} checks")
465
570
  raise typer.Exit(1)
466
571
 
467
- if check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
468
- self.display_error(f"Playwright script is required for {check_type} checks. Use --pw-script-file or --parameters with pw_script")
572
+ if actual_check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
573
+ self.display_error(f"Playwright script is required for {actual_check_type} checks. Use --pw-script-file or --parameters with pw_script")
469
574
  raise typer.Exit(1)
470
575
 
471
576
  # Use the actual SDK method
472
577
  check = checks_api.v1_checks_post(monitor_check=check_data)
473
578
 
474
- # Build success message
475
- success_details = [f"ID: {check.id}", f"Type: {check_type}"]
579
+ # Build success message - use the actual name from check_data, not the parameter
580
+ actual_name = check_data.get("name", name)
581
+ actual_type = check_data.get("type", check_type)
582
+ success_details = [f"ID: {check.id}", f"Type: {actual_type}"]
476
583
  if url:
477
584
  success_details.append(f"URL: {url}")
478
585
  if host:
@@ -483,7 +590,7 @@ class ChecksCommand(BaseCommand):
483
590
  success_details.append(f"Script: loaded from {pw_script_file}")
484
591
 
485
592
  self.display_success(
486
- f"Check '{name}' created successfully!\n" + "\n".join(success_details),
593
+ f"Check '{actual_name}' created successfully!\n" + "\n".join(success_details),
487
594
  "✅ Check Created"
488
595
  )
489
596
 
@@ -1075,23 +1182,36 @@ def get_check(
1075
1182
 
1076
1183
  @app.command("create")
1077
1184
  def create_check(
1078
- name: str = typer.Option(..., "--name", "-n", help="Check name"),
1079
- check_type: str = typer.Option(..., "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
1185
+ name: str = typer.Option("", "--name", "-n", help="Check name (required if not using --from-file)"),
1186
+ check_type: str = typer.Option("web", "--type", "-t", help="Check type (web, api, tcp, ssl, synthetic, multistep)"),
1080
1187
  url: Optional[str] = typer.Option(None, "--url", "-u", help="URL to monitor (required for web, api, ssl checks)"),
1081
1188
  host: Optional[str] = typer.Option(None, "--host", help="Hostname/IP for TCP/SSL checks (max 255 characters)"),
1082
1189
  port: Optional[int] = typer.Option(None, "--port", help="Port number for TCP checks (1-65535)"),
1083
1190
  interval: int = typer.Option(300, "--interval", "-i", help="Check interval in seconds"),
1084
1191
  timeout: int = typer.Option(30, "--timeout", help="Timeout in seconds"),
1085
1192
  parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
1086
- pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks")
1193
+ pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
1194
+ from_file: Optional[str] = typer.Option(None, "--from-file", help="Path to JSON or YAML file containing check configuration")
1087
1195
  ):
1088
- """Create a new monitoring check. Parameters vary by check type:
1196
+ """Create a new monitoring check. Can be created from command line options or from a JSON/YAML file.
1197
+
1198
+ When using --from-file:
1199
+ - Command line options override file values
1200
+ - File should contain check configuration in JSON or YAML format
1201
+
1202
+ Parameters vary by check type:
1089
1203
  - web/api: --url required
1090
1204
  - tcp: --host required, --port optional
1091
1205
  - ssl: --url or --host required
1092
1206
  - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
1207
+
1208
+ # Validate that we have either from_file or required parameters
1209
+ if not from_file and not name:
1210
+ typer.echo("Error: --name is required when not using --from-file", err=True)
1211
+ raise typer.Exit(1)
1212
+
1093
1213
  checks_cmd = ChecksCommand(get_output_format())
1094
- checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file)
1214
+ checks_cmd.create_check(name, check_type, url, host, port, interval, timeout, parameters, pw_script_file, from_file)
1095
1215
 
1096
1216
 
1097
1217
  @app.command("update")
@@ -21,6 +21,48 @@ class OnDemandChecksCommand(BaseCommand):
21
21
 
22
22
  def __init__(self, output_format: Optional[str] = None):
23
23
  super().__init__(output_format)
24
+
25
+ def _parse_check_file(self, file_path: str) -> dict:
26
+ """Parse check configuration from JSON or YAML file"""
27
+ import json
28
+ import os
29
+
30
+ if not os.path.exists(file_path):
31
+ self.display_error(f"Check file not found: {file_path}")
32
+ raise typer.Exit(1)
33
+
34
+ try:
35
+ with open(file_path, 'r', encoding='utf-8') as f:
36
+ content = f.read().strip()
37
+
38
+ if not content:
39
+ self.display_error(f"Check file is empty: {file_path}")
40
+ raise typer.Exit(1)
41
+
42
+ # Try to determine format from file extension or content
43
+ file_ext = os.path.splitext(file_path)[1].lower()
44
+
45
+ if file_ext in ['.yaml', '.yml']:
46
+ try:
47
+ import yaml
48
+ return yaml.safe_load(content)
49
+ except ImportError:
50
+ self.display_error("YAML support not available. Install with: pip install pyyaml")
51
+ raise typer.Exit(1)
52
+ except yaml.YAMLError as e:
53
+ self.display_error(f"Invalid YAML in file {file_path}: {str(e)}")
54
+ raise typer.Exit(1)
55
+ else:
56
+ # Default to JSON
57
+ try:
58
+ return json.loads(content)
59
+ except json.JSONDecodeError as e:
60
+ self.display_error(f"Invalid JSON in file {file_path}: {str(e)}")
61
+ raise typer.Exit(1)
62
+
63
+ except IOError as e:
64
+ self.display_error(f"Failed to read check file: {str(e)}")
65
+ raise typer.Exit(1)
24
66
 
25
67
  def get_client(self):
26
68
  """Get Pingera SDK client with authentication for on-demand checks"""
@@ -48,7 +90,7 @@ class OnDemandChecksCommand(BaseCommand):
48
90
  self.display_error(f"Failed to initialize client: {str(e)}")
49
91
  raise typer.Exit(1)
50
92
 
51
- def execute_custom_check(self, url: Optional[str] = None, check_type: str = "web", host: Optional[str] = None, port: Optional[int] = None, timeout: int = 30, name: str = "On-demand check", parameters: Optional[str] = None, pw_script_file: Optional[str] = None, wait_for_result: bool = False):
93
+ 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 = False):
52
94
  """Execute custom on-demand check"""
53
95
  try:
54
96
  import json
@@ -58,12 +100,73 @@ class OnDemandChecksCommand(BaseCommand):
58
100
  # Import the SDK models
59
101
  from pingera.models import ExecuteCustomCheckRequest
60
102
 
61
- # Build check data
62
- check_data = {
63
- "name": name,
64
- "type": check_type,
65
- "timeout": timeout
66
- }
103
+ # If creating from file, parse file and merge with command line options
104
+ if from_file:
105
+ file_data = self._parse_check_file(from_file)
106
+
107
+ # Filter only SDK-recognized fields from file data
108
+ # This ignores marketplace-specific fields and other extensions
109
+ sdk_fields = ["name", "type", "url", "host", "port", "timeout", "parameters"]
110
+ filtered_file_data = {k: v for k, v in file_data.items() if k in sdk_fields}
111
+
112
+ # Command line options take precedence over file data
113
+ check_data = {
114
+ "name": filtered_file_data.get("name", name),
115
+ "type": filtered_file_data.get("type", check_type),
116
+ "timeout": filtered_file_data.get("timeout", timeout)
117
+ }
118
+
119
+ # Override with command line values if provided
120
+ if name != "On-demand check": # Only override if name was explicitly provided
121
+ check_data["name"] = name
122
+ if check_type != "web": # Only override if type was explicitly provided
123
+ check_data["type"] = check_type
124
+ if timeout != 30: # Only override if timeout was explicitly provided
125
+ check_data["timeout"] = timeout
126
+
127
+ # Handle URL from file
128
+ if url is None and filtered_file_data.get("url"):
129
+ url = filtered_file_data["url"]
130
+ elif url is not None:
131
+ # Command line URL takes precedence
132
+ pass
133
+
134
+ # Handle host/port from file
135
+ if host is None and filtered_file_data.get("host"):
136
+ host = filtered_file_data["host"]
137
+ elif host is not None:
138
+ # Command line host takes precedence
139
+ pass
140
+
141
+ if port is None and filtered_file_data.get("port"):
142
+ port = filtered_file_data["port"]
143
+ elif port is not None:
144
+ # Command line port takes precedence
145
+ pass
146
+
147
+ # Handle parameters from file
148
+ file_parameters = filtered_file_data.get("parameters")
149
+ if file_parameters and parameters is None:
150
+ # Use file parameters if no command line parameters
151
+ parameters = json.dumps(file_parameters) if isinstance(file_parameters, dict) else file_parameters
152
+
153
+ # Handle pw_script_file from file (this is CLI-specific, not sent to SDK)
154
+ if pw_script_file is None and file_data.get("pw_script_file"):
155
+ pw_script_file = file_data["pw_script_file"]
156
+
157
+ # Show info about ignored fields if any
158
+ ignored_fields = [k for k in file_data.keys() if k not in sdk_fields and k != "pw_script_file"]
159
+ if ignored_fields:
160
+ self.display_info(f"Ignoring non-SDK fields from file: {', '.join(ignored_fields)}")
161
+
162
+ self.display_info(f"Creating on-demand check from file: {from_file}")
163
+ else:
164
+ # Build check data normally
165
+ check_data = {
166
+ "name": name,
167
+ "type": check_type,
168
+ "timeout": timeout
169
+ }
67
170
 
68
171
  # Add URL if provided (required for web, api, ssl checks)
69
172
  if url is not None:
@@ -112,21 +215,22 @@ class OnDemandChecksCommand(BaseCommand):
112
215
  if params_dict:
113
216
  check_data["parameters"] = params_dict
114
217
 
115
- # Validation based on check type
116
- if check_type in ['web', 'api'] and not url:
117
- self.display_error(f"URL is required for {check_type} checks")
218
+ # Validation based on check type - use the actual type from check_data
219
+ actual_check_type = check_data.get("type", check_type)
220
+ if actual_check_type in ['web', 'api'] and not url:
221
+ self.display_error(f"URL is required for {actual_check_type} checks")
118
222
  raise typer.Exit(1)
119
223
 
120
- if check_type in ['tcp'] and not host:
121
- self.display_error(f"Host is required for {check_type} checks")
224
+ if actual_check_type in ['tcp'] and not host:
225
+ self.display_error(f"Host is required for {actual_check_type} checks")
122
226
  raise typer.Exit(1)
123
227
 
124
- if check_type in ['ssl'] and not (url or host):
125
- self.display_error(f"Either URL or host is required for {check_type} checks")
228
+ if actual_check_type in ['ssl'] and not (url or host):
229
+ self.display_error(f"Either URL or host is required for {actual_check_type} checks")
126
230
  raise typer.Exit(1)
127
231
 
128
- if check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
129
- self.display_error(f"Playwright script is required for {check_type} checks. Use --pw-script-file or --parameters with pw_script")
232
+ if actual_check_type in ['synthetic', 'multistep'] and not params_dict.get('pw_script'):
233
+ self.display_error(f"Playwright script is required for {actual_check_type} checks. Use --pw-script-file or --parameters with pw_script")
130
234
  raise typer.Exit(1)
131
235
 
132
236
  # Create the request
@@ -606,15 +710,22 @@ def run_custom_check(
606
710
  name: str = typer.Option("On-demand check", "--name", "-n", help="Check name"),
607
711
  parameters: Optional[str] = typer.Option(None, "--parameters", help="JSON string with check parameters (e.g., '{\"regions\": [\"US\", \"EU\"]}')"),
608
712
  pw_script_file: Optional[str] = typer.Option(None, "--pw-script-file", help="Path to file containing Playwright script for synthetic/multistep checks"),
713
+ from_file: Optional[str] = typer.Option(None, "--from-file", help="Path to JSON or YAML file containing check configuration"),
609
714
  wait_for_result: bool = typer.Option(False, "--wait-for-result", help="Wait for job completion and show result immediately (max 5 minutes)"),
610
715
  ):
611
- """Execute custom on-demand check. Parameters vary by check type:
716
+ """Execute custom on-demand check. Can be executed from command line options or from a JSON/YAML file.
717
+
718
+ When using --from-file:
719
+ - Command line options override file values
720
+ - File should contain check configuration in JSON or YAML format
721
+
722
+ Parameters vary by check type:
612
723
  - web/api: --url required
613
724
  - tcp: --host required, --port optional
614
725
  - ssl: --url or --host required
615
726
  - synthetic/multistep: --pw-script-file or --parameters with pw_script required"""
616
727
  on_demand_cmd = OnDemandChecksCommand(get_output_format())
617
- on_demand_cmd.execute_custom_check(url, check_type, host, port, timeout, name, parameters, pw_script_file, wait_for_result)
728
+ on_demand_cmd.execute_custom_check(url, check_type, host, port, timeout, name, parameters, pw_script_file, from_file, wait_for_result)
618
729
 
619
730
 
620
731
  @run_app.command("existing")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingera-cli
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -8,6 +8,8 @@ replit.md
8
8
  run_tests.py
9
9
  setup.py
10
10
  docs/cli-commands.md
11
+ docs/example-check.yaml
12
+ docs/example-synthetic-screenshot.json
11
13
  pingera_cli/__init__.py
12
14
  pingera_cli/__main__.py
13
15
  pingera_cli/main.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes