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.
- {pingera_cli-0.1.5/pingera_cli.egg-info → pingera_cli-0.1.7}/PKG-INFO +1 -1
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/docs/cli-commands.md +93 -7
- pingera_cli-0.1.7/docs/example-check.yaml +21 -0
- pingera_cli-0.1.7/docs/example-synthetic-screenshot.json +21 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/__init__.py +1 -1
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/checks.py +145 -25
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/on_demand_checks.py +129 -18
- {pingera_cli-0.1.5 → pingera_cli-0.1.7/pingera_cli.egg-info}/PKG-INFO +1 -1
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/SOURCES.txt +2 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/.gitignore +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/.replit +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/LICENSE +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/README.md +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/assert +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/__main__.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/__init__.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/auth.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/base.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/check_groups.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/check_secrets.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/commands/secrets.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/__init__.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/base_formatter.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/generic_formatter.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/multistep_formatter.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/registry.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/ssl_formatter.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/synthetic_formatter.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/formatters/web_formatter.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/main.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/utils/__init__.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/utils/config.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli/utils/console.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/dependency_links.txt +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/entry_points.txt +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/requires.txt +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pingera_cli.egg-info/top_level.txt +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/pyproject.toml +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/replit.md +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/run_tests.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/setup.cfg +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/setup.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/__init__.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/conftest.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/requirements.txt +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_auth.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_checks.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_config.py +0 -0
- {pingera_cli-0.1.5 → pingera_cli-0.1.7}/tests/test_on_demand_checks.py +0 -0
|
@@ -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
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
-
##
|
|
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 "
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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
|
|
460
|
-
self.display_error(f"Host is required for {
|
|
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
|
|
464
|
-
self.display_error(f"Either URL or host is required for {
|
|
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
|
|
468
|
-
self.display_error(f"Playwright script is required for {
|
|
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
|
-
|
|
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 '{
|
|
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(
|
|
1079
|
-
check_type: str = typer.Option(
|
|
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.
|
|
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
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
121
|
-
self.display_error(f"Host is required for {
|
|
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
|
|
125
|
-
self.display_error(f"Either URL or host is required for {
|
|
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
|
|
129
|
-
self.display_error(f"Playwright script is required for {
|
|
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.
|
|
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")
|
|
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
|
|
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
|
|
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
|
|
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
|