pingera-cli 0.1.15__tar.gz → 0.1.16__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.15/pingera_cli.egg-info → pingera_cli-0.1.16}/PKG-INFO +2 -2
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/cli-commands.md +39 -21
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/__init__.py +1 -1
- pingera_cli-0.1.16/pingera_cli/commands/components.py +539 -0
- pingera_cli-0.1.16/pingera_cli/commands/incidents.py +578 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/on_demand_checks.py +5 -0
- pingera_cli-0.1.16/pingera_cli/commands/pages.py +810 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/main.py +18 -15
- {pingera_cli-0.1.15 → pingera_cli-0.1.16/pingera_cli.egg-info}/PKG-INFO +2 -2
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/SOURCES.txt +3 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/requires.txt +1 -1
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pyproject.toml +1 -1
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/.gitignore +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/.replit +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/LICENSE +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/README.md +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/assert +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/example-check.yaml +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/example-synthetic-screenshot.json +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/example-web-check.json +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/__main__.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/__init__.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/auth.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/base.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/check_groups.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/check_secrets.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/checks.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/execution_groups.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/secrets.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/__init__.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/base_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/dns_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/generic_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/icmp_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/multistep_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/portscan_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/registry.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/ssl_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/synthetic_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/web_formatter.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/__init__.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/config.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/console.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/file_utils.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/dependency_links.txt +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/entry_points.txt +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/top_level.txt +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pwscript.js +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/replit.md +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/run_tests.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/setup.cfg +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/setup.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/__init__.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/conftest.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/requirements.txt +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/test_auth.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/test_checks.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/test_config.py +0 -0
- {pingera_cli-0.1.15 → pingera_cli-0.1.16}/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.
|
|
3
|
+
Version: 0.1.16
|
|
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.
|
|
34
|
+
Requires-Dist: pingera-sdk>=1.0.15
|
|
35
35
|
Requires-Dist: click>=8.0.0
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
@@ -195,7 +195,7 @@ pngr heartbeats ping <heartbeat-id>
|
|
|
195
195
|
pngr heartbeats logs <heartbeat-id> [--from <date>] [--to <date>] [--page <num>]
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
## Status Pages Commands
|
|
198
|
+
## Status Pages Commands
|
|
199
199
|
|
|
200
200
|
### `pngr pages`
|
|
201
201
|
Manage status pages.
|
|
@@ -207,43 +207,40 @@ pngr pages list [--page <num>] [--page-size <size>]
|
|
|
207
207
|
# Get specific status page
|
|
208
208
|
pngr pages get <page-id>
|
|
209
209
|
|
|
210
|
-
# Create status page
|
|
211
|
-
pngr pages create --name <
|
|
210
|
+
# Create a new status page
|
|
211
|
+
pngr pages create --name "My Status Page" [--subdomain <sub>] [--description <desc>] [--headline <text>] [--url <url>] [--private] [--timezone <tz>]
|
|
212
212
|
|
|
213
|
-
# Update status page
|
|
214
|
-
pngr pages update <page-id> --name <name> --description <desc> --
|
|
213
|
+
# Update an existing status page
|
|
214
|
+
pngr pages update <page-id> [--name <name>] [--subdomain <sub>] [--description <desc>] [--headline <text>] [--url <url>] [--public/--private] [--timezone <tz>]
|
|
215
215
|
|
|
216
|
-
# Delete status page
|
|
216
|
+
# Delete a status page
|
|
217
217
|
pngr pages delete <page-id> [--confirm]
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
### `pngr components`
|
|
221
|
-
Manage status page components.
|
|
220
|
+
### `pngr pages components`
|
|
221
|
+
Manage status page components (nested under pages).
|
|
222
222
|
|
|
223
223
|
```bash
|
|
224
|
-
# List components
|
|
225
|
-
pngr components list
|
|
224
|
+
# List components for a page
|
|
225
|
+
pngr pages components list --page-id <page-id>
|
|
226
226
|
|
|
227
227
|
# Get specific component
|
|
228
|
-
pngr components get <component-id>
|
|
228
|
+
pngr pages components get <component-id> --page-id <page-id>
|
|
229
229
|
|
|
230
230
|
# Create component
|
|
231
|
-
pngr components create --name <name> [--description <desc>] [--status <status>] [--
|
|
231
|
+
pngr pages components create --name <name> --page-id <page-id> [--description <desc>] [--status <status>] [--group-id <id>] [--position <num>] [--showcase] [--only-if-degraded] [--start-date <YYYY-MM-DD>]
|
|
232
232
|
|
|
233
233
|
# Update component
|
|
234
|
-
pngr components update <component-id> --name <name> --description <desc> --
|
|
234
|
+
pngr pages components update <component-id> --page-id <page-id> [--name <name>] [--description <desc>] [--status <status>] [--group-id <id>] [--position <num>] [--showcase/--no-showcase] [--only-if-degraded/--always-show]
|
|
235
235
|
|
|
236
236
|
# Delete component
|
|
237
|
-
pngr components delete <component-id>
|
|
238
|
-
|
|
239
|
-
# Update component status
|
|
240
|
-
pngr components status <component-id> --status <status> [--page-id <id>]
|
|
237
|
+
pngr pages components delete <component-id> --page-id <page-id> [--confirm]
|
|
241
238
|
|
|
242
239
|
# Get component uptime
|
|
243
|
-
pngr components uptime <component-id>
|
|
240
|
+
pngr pages components uptime <component-id> --page-id <page-id> [--start <date>] [--end <date>]
|
|
244
241
|
|
|
245
|
-
#
|
|
246
|
-
pngr components uptime-bulk --
|
|
242
|
+
# Get uptime for all components (bulk)
|
|
243
|
+
pngr pages components uptime-bulk --page-id <page-id> [--start <date>] [--end <date>]
|
|
247
244
|
```
|
|
248
245
|
|
|
249
246
|
### `pngr incidents`
|
|
@@ -515,11 +512,32 @@ pngr checks run custom --url https://example.com --type ssl --name "SSL Certific
|
|
|
515
512
|
# Execute existing check and wait for result
|
|
516
513
|
pngr checks run existing check_123 --wait-for-result
|
|
517
514
|
|
|
515
|
+
# Update a status page
|
|
516
|
+
pngr pages update page_123 --name "Updated Name" --description "New description"
|
|
517
|
+
|
|
518
|
+
# Make a page public
|
|
519
|
+
pngr pages update page_123 --public
|
|
520
|
+
|
|
521
|
+
# Update page timezone and language
|
|
522
|
+
pngr pages update page_123 --timezone "America/New_York" --language "en"
|
|
523
|
+
|
|
518
524
|
# Create an incident
|
|
519
525
|
pngr incidents create --name "Database Issues" --body "Investigating connectivity" --status investigating --impact major
|
|
520
526
|
|
|
521
527
|
# Update component status
|
|
522
|
-
pngr components
|
|
528
|
+
pngr pages components update comp_123 --page-id page_123 --status degraded_performance
|
|
529
|
+
|
|
530
|
+
# Get uptime for a specific component
|
|
531
|
+
pngr pages components uptime comp_123 --page-id page_123
|
|
532
|
+
|
|
533
|
+
# Get uptime for a specific component with date range
|
|
534
|
+
pngr pages components uptime comp_123 --page-id page_123 --start 2024-01-01 --end 2024-01-31
|
|
535
|
+
|
|
536
|
+
# Get uptime for all components on a page
|
|
537
|
+
pngr pages components uptime-bulk --page-id page_123
|
|
538
|
+
|
|
539
|
+
# Get uptime for all components with date range
|
|
540
|
+
pngr pages components uptime-bulk --page-id page_123 --start 2024-01-01 --end 2024-01-31
|
|
523
541
|
|
|
524
542
|
# Update check with custom parameters (Playwright script and regions)
|
|
525
543
|
pngr checks update check_123 --parameters '{"pw_script": "const { test, expect } = require(\"@playwright/test\"); test(\"example\", async ({ page }) => { await page.goto(\"https://example.com\"); await expect(page).toHaveTitle(/Example/); });", "regions": ["US", "EU"]}'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Pingera CLI - A beautiful CLI tool for Pingera Platform
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.1.
|
|
5
|
+
__version__ = "0.1.16"
|
|
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
|
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Components commands for PingeraCLI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.prompt import Confirm
|
|
13
|
+
|
|
14
|
+
from .base import BaseCommand
|
|
15
|
+
from ..utils.config import get_api_key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ComponentsCommand(BaseCommand):
|
|
19
|
+
"""
|
|
20
|
+
Commands for managing status page components
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, output_format: Optional[str] = None, verbose: bool = False):
|
|
24
|
+
super().__init__(output_format)
|
|
25
|
+
self.verbose = verbose
|
|
26
|
+
|
|
27
|
+
def get_client(self):
|
|
28
|
+
"""Get Pingera SDK client with authentication"""
|
|
29
|
+
api_key = get_api_key()
|
|
30
|
+
if not api_key:
|
|
31
|
+
self.display_error("API key not found. Use 'pngr auth login --api-key <key>' to set it.")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from pingera import ApiClient, Configuration
|
|
36
|
+
from pingera.api import StatusPagesComponentsApi
|
|
37
|
+
from ..utils.config import get_config
|
|
38
|
+
|
|
39
|
+
# Configure the client
|
|
40
|
+
configuration = Configuration()
|
|
41
|
+
configuration.host = get_config().get('base_url', 'https://api.pingera.ru')
|
|
42
|
+
configuration.api_key['apiKeyAuth'] = api_key
|
|
43
|
+
|
|
44
|
+
# Create API client
|
|
45
|
+
api_client = ApiClient(configuration)
|
|
46
|
+
return StatusPagesComponentsApi(api_client)
|
|
47
|
+
except ImportError:
|
|
48
|
+
self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.display_error(f"Failed to initialize client: {str(e)}")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
def list_components(self, page_id: str):
|
|
55
|
+
"""List components for a status page"""
|
|
56
|
+
try:
|
|
57
|
+
components_api = self.get_client()
|
|
58
|
+
response = components_api.v1_pages_page_id_components_get(page_id=page_id)
|
|
59
|
+
|
|
60
|
+
if not response:
|
|
61
|
+
if self.output_format in ['json', 'yaml']:
|
|
62
|
+
self.output_data({"components": [], "total": 0, "message": "No components found"})
|
|
63
|
+
else:
|
|
64
|
+
self.display_info("No components found.")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Prepare data for different output formats
|
|
68
|
+
if self.output_format in ['json', 'yaml']:
|
|
69
|
+
components_data = []
|
|
70
|
+
for component in response:
|
|
71
|
+
component_dict = {
|
|
72
|
+
"id": str(component.id) if hasattr(component, 'id') and component.id else None,
|
|
73
|
+
"name": component.name if hasattr(component, 'name') and component.name else None,
|
|
74
|
+
"description": component.description if hasattr(component, 'description') and component.description else None,
|
|
75
|
+
"status": component.status if hasattr(component, 'status') and component.status else None,
|
|
76
|
+
"group_id": component.group_id if hasattr(component, 'group_id') and component.group_id else None,
|
|
77
|
+
"position": component.position if hasattr(component, 'position') else None,
|
|
78
|
+
"showcase": component.showcase if hasattr(component, 'showcase') else None,
|
|
79
|
+
"created_at": component.created_at.isoformat() if hasattr(component, 'created_at') and component.created_at else None,
|
|
80
|
+
}
|
|
81
|
+
components_data.append(component_dict)
|
|
82
|
+
|
|
83
|
+
self.output_data({
|
|
84
|
+
"components": components_data,
|
|
85
|
+
"total": len(components_data),
|
|
86
|
+
"page_id": page_id
|
|
87
|
+
})
|
|
88
|
+
else:
|
|
89
|
+
# Create table for default output
|
|
90
|
+
table = Table(title=f"Components for Page {page_id}")
|
|
91
|
+
table.add_column("ID", style="cyan", min_width=15)
|
|
92
|
+
table.add_column("Name", style="green", min_width=20)
|
|
93
|
+
table.add_column("Status", style="yellow", min_width=12)
|
|
94
|
+
table.add_column("Group", style="blue")
|
|
95
|
+
table.add_column("Position", style="magenta")
|
|
96
|
+
table.add_column("Showcase", style="dim")
|
|
97
|
+
|
|
98
|
+
for component in response:
|
|
99
|
+
component_id = str(component.id) if hasattr(component, 'id') and component.id else "-"
|
|
100
|
+
name = str(component.name) if hasattr(component, 'name') and component.name else "-"
|
|
101
|
+
status = str(component.status) if hasattr(component, 'status') and component.status else "-"
|
|
102
|
+
group_id = str(component.group_id) if hasattr(component, 'group_id') and component.group_id else "-"
|
|
103
|
+
position = str(component.position) if hasattr(component, 'position') else "-"
|
|
104
|
+
showcase = "✓" if hasattr(component, 'showcase') and component.showcase else ""
|
|
105
|
+
|
|
106
|
+
table.add_row(
|
|
107
|
+
component_id,
|
|
108
|
+
name,
|
|
109
|
+
status,
|
|
110
|
+
group_id,
|
|
111
|
+
position,
|
|
112
|
+
showcase
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.console.print(table)
|
|
116
|
+
self.console.print(f"\n[dim]Found {len(response)} components[/dim]")
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
self.display_error(f"Failed to list components: {str(e)}")
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
|
|
122
|
+
def get_component(self, page_id: str, component_id: str):
|
|
123
|
+
"""Get specific component details"""
|
|
124
|
+
try:
|
|
125
|
+
components_api = self.get_client()
|
|
126
|
+
component = components_api.v1_pages_page_id_components_component_id_get(
|
|
127
|
+
page_id=page_id,
|
|
128
|
+
component_id=component_id
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Prepare data for different output formats
|
|
132
|
+
if self.output_format in ['json', 'yaml']:
|
|
133
|
+
component_data = {
|
|
134
|
+
"id": str(component.id) if hasattr(component, 'id') and component.id else None,
|
|
135
|
+
"name": component.name if hasattr(component, 'name') and component.name else None,
|
|
136
|
+
"description": component.description if hasattr(component, 'description') and component.description else None,
|
|
137
|
+
"status": component.status if hasattr(component, 'status') and component.status else None,
|
|
138
|
+
"group_id": component.group_id if hasattr(component, 'group_id') and component.group_id else None,
|
|
139
|
+
"position": component.position if hasattr(component, 'position') else None,
|
|
140
|
+
"showcase": component.showcase if hasattr(component, 'showcase') else None,
|
|
141
|
+
"only_show_if_degraded": component.only_show_if_degraded if hasattr(component, 'only_show_if_degraded') else None,
|
|
142
|
+
"start_date": component.start_date.isoformat() if hasattr(component, 'start_date') and component.start_date else None,
|
|
143
|
+
"created_at": component.created_at.isoformat() if hasattr(component, 'created_at') and component.created_at else None,
|
|
144
|
+
"updated_at": component.updated_at.isoformat() if hasattr(component, 'updated_at') and component.updated_at else None,
|
|
145
|
+
}
|
|
146
|
+
self.output_data(component_data)
|
|
147
|
+
else:
|
|
148
|
+
# Rich formatted output
|
|
149
|
+
basic_info = f"""[bold cyan]Basic Information:[/bold cyan]
|
|
150
|
+
• ID: [white]{component.id}[/white]
|
|
151
|
+
• Name: [white]{component.name}[/white]
|
|
152
|
+
• Status: [white]{component.status if hasattr(component, 'status') and component.status else 'Not set'}[/white]
|
|
153
|
+
• Description: [white]{component.description if hasattr(component, 'description') and component.description else 'Not set'}[/white]"""
|
|
154
|
+
|
|
155
|
+
settings_info = f"""
|
|
156
|
+
[bold cyan]Settings:[/bold cyan]
|
|
157
|
+
• Position: [white]{component.position if hasattr(component, 'position') else 'Not set'}[/white]
|
|
158
|
+
• Group ID: [white]{component.group_id if hasattr(component, 'group_id') and component.group_id else 'None'}[/white]
|
|
159
|
+
• Showcase: [white]{'Yes' if hasattr(component, 'showcase') and component.showcase else 'No'}[/white]
|
|
160
|
+
• Only show if degraded: [white]{'Yes' if hasattr(component, 'only_show_if_degraded') and component.only_show_if_degraded else 'No'}[/white]
|
|
161
|
+
• Start date: [white]{component.start_date.strftime('%Y-%m-%d') if hasattr(component, 'start_date') and component.start_date else 'Not set'}[/white]"""
|
|
162
|
+
|
|
163
|
+
timestamps = f"""
|
|
164
|
+
[bold cyan]Timestamps:[/bold cyan]
|
|
165
|
+
• Created: [white]{component.created_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(component, 'created_at') and component.created_at else 'Unknown'}[/white]
|
|
166
|
+
• Updated: [white]{component.updated_at.strftime('%Y-%m-%d %H:%M:%S UTC') if hasattr(component, 'updated_at') and component.updated_at else 'Unknown'}[/white]"""
|
|
167
|
+
|
|
168
|
+
full_info = f"{basic_info}{settings_info}{timestamps}"
|
|
169
|
+
|
|
170
|
+
panel = Panel(
|
|
171
|
+
full_info,
|
|
172
|
+
title=f"🔧 Component: {component.name}",
|
|
173
|
+
border_style="blue",
|
|
174
|
+
padding=(1, 2),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self.console.print(panel)
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.display_error(f"Failed to get component: {str(e)}")
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
def create_component(
|
|
184
|
+
self,
|
|
185
|
+
page_id: str,
|
|
186
|
+
name: str,
|
|
187
|
+
description: Optional[str] = None,
|
|
188
|
+
status: Optional[str] = None,
|
|
189
|
+
group_id: Optional[str] = None,
|
|
190
|
+
position: Optional[int] = None,
|
|
191
|
+
showcase: bool = False,
|
|
192
|
+
only_show_if_degraded: bool = False,
|
|
193
|
+
start_date: Optional[str] = None,
|
|
194
|
+
):
|
|
195
|
+
"""Create a new component"""
|
|
196
|
+
try:
|
|
197
|
+
components_api = self.get_client()
|
|
198
|
+
|
|
199
|
+
# Build component data
|
|
200
|
+
component_data = {
|
|
201
|
+
"name": name,
|
|
202
|
+
"showcase": showcase,
|
|
203
|
+
"only_show_if_degraded": only_show_if_degraded,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Add optional fields
|
|
207
|
+
if description:
|
|
208
|
+
component_data["description"] = description
|
|
209
|
+
if status:
|
|
210
|
+
component_data["status"] = status
|
|
211
|
+
if group_id:
|
|
212
|
+
component_data["group_id"] = group_id
|
|
213
|
+
if position is not None:
|
|
214
|
+
component_data["position"] = position
|
|
215
|
+
if start_date:
|
|
216
|
+
component_data["start_date"] = start_date
|
|
217
|
+
|
|
218
|
+
# Create the component
|
|
219
|
+
component = components_api.v1_pages_page_id_components_post(
|
|
220
|
+
page_id=page_id,
|
|
221
|
+
component=component_data
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Build success message
|
|
225
|
+
success_details = [
|
|
226
|
+
f"ID: {component.id}",
|
|
227
|
+
f"Name: {name}",
|
|
228
|
+
f"Status: {status or 'operational'}"
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
self.display_success(
|
|
232
|
+
f"Component '{name}' created successfully!\n" + "\n".join(success_details),
|
|
233
|
+
"✅ Component Created"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.display_error(f"Failed to create component: {str(e)}")
|
|
238
|
+
raise typer.Exit(1)
|
|
239
|
+
|
|
240
|
+
def update_component(
|
|
241
|
+
self,
|
|
242
|
+
page_id: str,
|
|
243
|
+
component_id: str,
|
|
244
|
+
name: Optional[str] = None,
|
|
245
|
+
description: Optional[str] = None,
|
|
246
|
+
status: Optional[str] = None,
|
|
247
|
+
group_id: Optional[str] = None,
|
|
248
|
+
position: Optional[int] = None,
|
|
249
|
+
showcase: Optional[bool] = None,
|
|
250
|
+
only_show_if_degraded: Optional[bool] = None,
|
|
251
|
+
):
|
|
252
|
+
"""Update an existing component"""
|
|
253
|
+
try:
|
|
254
|
+
components_api = self.get_client()
|
|
255
|
+
|
|
256
|
+
# Build component data with only provided fields
|
|
257
|
+
component_data = {}
|
|
258
|
+
|
|
259
|
+
if name is not None:
|
|
260
|
+
component_data["name"] = name
|
|
261
|
+
if description is not None:
|
|
262
|
+
component_data["description"] = description
|
|
263
|
+
if status is not None:
|
|
264
|
+
component_data["status"] = status
|
|
265
|
+
if group_id is not None:
|
|
266
|
+
component_data["group_id"] = group_id
|
|
267
|
+
if position is not None:
|
|
268
|
+
component_data["position"] = position
|
|
269
|
+
if showcase is not None:
|
|
270
|
+
component_data["showcase"] = showcase
|
|
271
|
+
if only_show_if_degraded is not None:
|
|
272
|
+
component_data["only_show_if_degraded"] = only_show_if_degraded
|
|
273
|
+
|
|
274
|
+
if not component_data:
|
|
275
|
+
self.display_error("No fields provided to update")
|
|
276
|
+
raise typer.Exit(1)
|
|
277
|
+
|
|
278
|
+
# Update the component
|
|
279
|
+
component = components_api.v1_pages_page_id_components_component_id_patch(
|
|
280
|
+
page_id=page_id,
|
|
281
|
+
component_id=component_id,
|
|
282
|
+
component1=component_data
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
self.display_success(
|
|
286
|
+
f"Component '{component_id}' updated successfully!",
|
|
287
|
+
"✅ Component Updated"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
self.display_error(f"Failed to update component: {str(e)}")
|
|
292
|
+
raise typer.Exit(1)
|
|
293
|
+
|
|
294
|
+
def delete_component(self, page_id: str, component_id: str, confirm: bool = False):
|
|
295
|
+
"""Delete a component"""
|
|
296
|
+
try:
|
|
297
|
+
if not confirm:
|
|
298
|
+
if not Confirm.ask(f"Are you sure you want to delete component {component_id}? This action cannot be undone."):
|
|
299
|
+
self.console.print("[yellow]Operation cancelled.[/yellow]")
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
components_api = self.get_client()
|
|
303
|
+
components_api.v1_pages_page_id_components_component_id_delete(
|
|
304
|
+
page_id=page_id,
|
|
305
|
+
component_id=component_id
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
self.display_success(
|
|
309
|
+
f"Component {component_id} deleted successfully!",
|
|
310
|
+
"✅ Component Deleted"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.display_error(f"Failed to delete component: {str(e)}")
|
|
315
|
+
raise typer.Exit(1)
|
|
316
|
+
|
|
317
|
+
def get_component_uptime(self, page_id: str, component_id: str, start: Optional[str] = None, end: Optional[str] = None):
|
|
318
|
+
"""Get uptime data for a specific component"""
|
|
319
|
+
try:
|
|
320
|
+
components_api = self.get_client()
|
|
321
|
+
|
|
322
|
+
# Build parameters
|
|
323
|
+
params = {}
|
|
324
|
+
if start:
|
|
325
|
+
params['start'] = start
|
|
326
|
+
if end:
|
|
327
|
+
params['end'] = end
|
|
328
|
+
|
|
329
|
+
# Make API call
|
|
330
|
+
uptime_data = components_api.v1_pages_page_id_components_component_id_uptime_get(
|
|
331
|
+
page_id=page_id,
|
|
332
|
+
component_id=component_id,
|
|
333
|
+
**params
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
print(f"##### {uptime_data}")
|
|
337
|
+
|
|
338
|
+
# Check if we got valid data
|
|
339
|
+
if uptime_data is None:
|
|
340
|
+
self.display_error("No uptime data returned from API")
|
|
341
|
+
raise typer.Exit(1)
|
|
342
|
+
|
|
343
|
+
# Prepare data for different output formats
|
|
344
|
+
if self.output_format in ['json', 'yaml']:
|
|
345
|
+
uptime_dict = {
|
|
346
|
+
"component_id": component_id,
|
|
347
|
+
"page_id": page_id,
|
|
348
|
+
"name": uptime_data.name if hasattr(uptime_data, 'name') and uptime_data.name else None,
|
|
349
|
+
"uptime_percentage": uptime_data.uptime_percentage if hasattr(uptime_data, 'uptime_percentage') and uptime_data.uptime_percentage is not None else None,
|
|
350
|
+
"range_start": uptime_data.range_start if hasattr(uptime_data, 'range_start') and uptime_data.range_start else start,
|
|
351
|
+
"range_end": uptime_data.range_end if hasattr(uptime_data, 'range_end') and uptime_data.range_end else end,
|
|
352
|
+
"major_outage": uptime_data.major_outage if hasattr(uptime_data, 'major_outage') and uptime_data.major_outage is not None else 0,
|
|
353
|
+
"partial_outage": uptime_data.partial_outage if hasattr(uptime_data, 'partial_outage') and uptime_data.partial_outage is not None else 0,
|
|
354
|
+
"related_events": uptime_data.related_events.id if hasattr(uptime_data, 'related_events') and hasattr(uptime_data.related_events, 'id') else [],
|
|
355
|
+
"warnings": uptime_data.warnings if hasattr(uptime_data, 'warnings') and uptime_data.warnings else [],
|
|
356
|
+
}
|
|
357
|
+
self.output_data(uptime_dict)
|
|
358
|
+
else:
|
|
359
|
+
# Rich formatted output
|
|
360
|
+
uptime_pct = uptime_data.uptime_percentage if hasattr(uptime_data, 'uptime_percentage') and uptime_data.uptime_percentage is not None else 0
|
|
361
|
+
|
|
362
|
+
# Color code based on uptime percentage
|
|
363
|
+
if uptime_pct >= 99.9:
|
|
364
|
+
uptime_color = "green"
|
|
365
|
+
elif uptime_pct >= 99.0:
|
|
366
|
+
uptime_color = "yellow"
|
|
367
|
+
else:
|
|
368
|
+
uptime_color = "red"
|
|
369
|
+
|
|
370
|
+
# Component name
|
|
371
|
+
component_name = uptime_data.name if hasattr(uptime_data, 'name') and uptime_data.name else "Unknown"
|
|
372
|
+
|
|
373
|
+
uptime_info = f"""[bold cyan]Component:[/bold cyan]
|
|
374
|
+
• Name: [white]{component_name}[/white]
|
|
375
|
+
• ID: [white]{component_id}[/white]
|
|
376
|
+
|
|
377
|
+
[bold cyan]Uptime Statistics:[/bold cyan]
|
|
378
|
+
• Uptime: [{uptime_color}]{uptime_pct:.3f}%[/{uptime_color}]"""
|
|
379
|
+
|
|
380
|
+
# Date range
|
|
381
|
+
range_start = uptime_data.range_start if hasattr(uptime_data, 'range_start') and uptime_data.range_start else start
|
|
382
|
+
range_end = uptime_data.range_end if hasattr(uptime_data, 'range_end') and uptime_data.range_end else end
|
|
383
|
+
|
|
384
|
+
if range_start or range_end:
|
|
385
|
+
uptime_info += f"""
|
|
386
|
+
• Period: [white]{range_start or 'Not specified'}[/white] to [white]{range_end or 'Not specified'}[/white]"""
|
|
387
|
+
|
|
388
|
+
# Outage counts
|
|
389
|
+
major_outage = uptime_data.major_outage if hasattr(uptime_data, 'major_outage') and uptime_data.major_outage is not None else 0
|
|
390
|
+
partial_outage = uptime_data.partial_outage if hasattr(uptime_data, 'partial_outage') and uptime_data.partial_outage is not None else 0
|
|
391
|
+
|
|
392
|
+
major_color = "red" if major_outage > 0 else "green"
|
|
393
|
+
partial_color = "yellow" if partial_outage > 0 else "green"
|
|
394
|
+
|
|
395
|
+
uptime_info += f"""
|
|
396
|
+
• Major Outages: [{major_color}]{major_outage}[/{major_color}]
|
|
397
|
+
• Partial Outages: [{partial_color}]{partial_outage}[/{partial_color}]"""
|
|
398
|
+
|
|
399
|
+
# Related events
|
|
400
|
+
if hasattr(uptime_data, 'related_events') and uptime_data.related_events and hasattr(uptime_data.related_events, 'id') and uptime_data.related_events.id:
|
|
401
|
+
event_ids = uptime_data.related_events.id
|
|
402
|
+
uptime_info += f"""
|
|
403
|
+
|
|
404
|
+
[bold cyan]Related Events:[/bold cyan]
|
|
405
|
+
• Event Count: [white]{len(event_ids)}[/white]"""
|
|
406
|
+
if self.verbose:
|
|
407
|
+
for event_id in event_ids[:10]: # Show first 10 in verbose mode
|
|
408
|
+
uptime_info += f"\n • [dim]{event_id}[/dim]"
|
|
409
|
+
if len(event_ids) > 10:
|
|
410
|
+
uptime_info += f"\n • [dim]... and {len(event_ids) - 10} more[/dim]"
|
|
411
|
+
else:
|
|
412
|
+
uptime_info += "\n[dim] Use --verbose to see event IDs[/dim]"
|
|
413
|
+
|
|
414
|
+
# Warnings
|
|
415
|
+
if hasattr(uptime_data, 'warnings') and uptime_data.warnings:
|
|
416
|
+
uptime_info += f"""
|
|
417
|
+
|
|
418
|
+
[bold yellow]Warnings:[/bold yellow]"""
|
|
419
|
+
for warning in uptime_data.warnings:
|
|
420
|
+
uptime_info += f"\n• [yellow]{warning}[/yellow]"
|
|
421
|
+
|
|
422
|
+
panel = Panel(
|
|
423
|
+
uptime_info,
|
|
424
|
+
title=f"📊 Component Uptime: {component_name}",
|
|
425
|
+
border_style="blue",
|
|
426
|
+
padding=(1, 2),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
self.console.print(panel)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
self.display_error(f"Failed to get component uptime: {str(e)}")
|
|
433
|
+
raise typer.Exit(1)
|
|
434
|
+
|
|
435
|
+
# Create Typer app for components commands
|
|
436
|
+
app = typer.Typer(name="components", help="🔧 Manage status page components", no_args_is_help=True)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@app.command("list")
|
|
440
|
+
def list_components(
|
|
441
|
+
page_id: str = typer.Option(..., "--page-id", "-p", help="Status page ID"),
|
|
442
|
+
):
|
|
443
|
+
"""List components for a status page"""
|
|
444
|
+
from ..utils.config import get_output_format
|
|
445
|
+
components_cmd = ComponentsCommand(get_output_format())
|
|
446
|
+
components_cmd.list_components(page_id)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@app.command("get")
|
|
450
|
+
def get_component(
|
|
451
|
+
component_id: str = typer.Argument(..., help="Component ID to retrieve"),
|
|
452
|
+
page_id: str = typer.Option(..., "--page-id", "-p", help="Status page ID"),
|
|
453
|
+
):
|
|
454
|
+
"""Get specific component details"""
|
|
455
|
+
from ..utils.config import get_output_format
|
|
456
|
+
components_cmd = ComponentsCommand(get_output_format())
|
|
457
|
+
components_cmd.get_component(page_id, component_id)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@app.command("create")
|
|
461
|
+
def create_component(
|
|
462
|
+
name: str = typer.Option(..., "--name", "-n", help="Component name"),
|
|
463
|
+
page_id: str = typer.Option(..., "--page-id", "-p", help="Status page ID"),
|
|
464
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="Component description"),
|
|
465
|
+
status: Optional[str] = typer.Option(None, "--status", "-s", help="Component status (operational, degraded_performance, partial_outage, major_outage)"),
|
|
466
|
+
group_id: Optional[str] = typer.Option(None, "--group-id", "-g", help="Component group ID"),
|
|
467
|
+
position: Optional[int] = typer.Option(None, "--position", help="Display position"),
|
|
468
|
+
showcase: bool = typer.Option(False, "--showcase", help="Prominently display on status page"),
|
|
469
|
+
only_show_if_degraded: bool = typer.Option(False, "--only-if-degraded", help="Only show when not operational"),
|
|
470
|
+
start_date: Optional[str] = typer.Option(None, "--start-date", help="Monitoring start date (YYYY-MM-DD)"),
|
|
471
|
+
):
|
|
472
|
+
"""Create a new component"""
|
|
473
|
+
from ..utils.config import get_output_format
|
|
474
|
+
components_cmd = ComponentsCommand(get_output_format())
|
|
475
|
+
components_cmd.create_component(
|
|
476
|
+
page_id=page_id,
|
|
477
|
+
name=name,
|
|
478
|
+
description=description,
|
|
479
|
+
status=status,
|
|
480
|
+
group_id=group_id,
|
|
481
|
+
position=position,
|
|
482
|
+
showcase=showcase,
|
|
483
|
+
only_show_if_degraded=only_show_if_degraded,
|
|
484
|
+
start_date=start_date,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@app.command("update")
|
|
489
|
+
def update_component(
|
|
490
|
+
component_id: str = typer.Argument(..., help="Component ID to update"),
|
|
491
|
+
page_id: str = typer.Option(..., "--page-id", "-p", help="Status page ID"),
|
|
492
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Component name"),
|
|
493
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="Component description"),
|
|
494
|
+
status: Optional[str] = typer.Option(None, "--status", "-s", help="Component status"),
|
|
495
|
+
group_id: Optional[str] = typer.Option(None, "--group-id", "-g", help="Component group ID"),
|
|
496
|
+
position: Optional[int] = typer.Option(None, "--position", help="Display position"),
|
|
497
|
+
showcase: Optional[bool] = typer.Option(None, "--showcase/--no-showcase", help="Prominently display on status page"),
|
|
498
|
+
only_show_if_degraded: Optional[bool] = typer.Option(None, "--only-if-degraded/--always-show", help="Only show when not operational"),
|
|
499
|
+
):
|
|
500
|
+
"""Update an existing component"""
|
|
501
|
+
from ..utils.config import get_output_format
|
|
502
|
+
components_cmd = ComponentsCommand(get_output_format())
|
|
503
|
+
components_cmd.update_component(
|
|
504
|
+
page_id=page_id,
|
|
505
|
+
component_id=component_id,
|
|
506
|
+
name=name,
|
|
507
|
+
description=description,
|
|
508
|
+
status=status,
|
|
509
|
+
group_id=group_id,
|
|
510
|
+
position=position,
|
|
511
|
+
showcase=showcase,
|
|
512
|
+
only_show_if_degraded=only_show_if_degraded,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@app.command("delete")
|
|
517
|
+
def delete_component(
|
|
518
|
+
component_id: str = typer.Argument(..., help="Component ID to delete"),
|
|
519
|
+
page_id: str = typer.Option(..., "--page-id", "-p", help="Status page ID"),
|
|
520
|
+
confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation prompt"),
|
|
521
|
+
):
|
|
522
|
+
"""Delete a component"""
|
|
523
|
+
from ..utils.config import get_output_format
|
|
524
|
+
components_cmd = ComponentsCommand(get_output_format())
|
|
525
|
+
components_cmd.delete_component(page_id, component_id, confirm)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@app.command("uptime")
|
|
529
|
+
def get_component_uptime(
|
|
530
|
+
component_id: str = typer.Argument(..., help="Component ID to get uptime for"),
|
|
531
|
+
page_id: str = typer.Option(..., "--page-id", "-p", help="Status page ID"),
|
|
532
|
+
start: Optional[str] = typer.Option(None, "--start", help="Start date (YYYY-MM-DD or ISO 8601)"),
|
|
533
|
+
end: Optional[str] = typer.Option(None, "--end", help="End date (YYYY-MM-DD or ISO 8601)"),
|
|
534
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information including event IDs"),
|
|
535
|
+
):
|
|
536
|
+
"""Get uptime data for a specific component"""
|
|
537
|
+
from ..utils.config import get_output_format
|
|
538
|
+
components_cmd = ComponentsCommand(get_output_format(), verbose=verbose)
|
|
539
|
+
components_cmd.get_component_uptime(page_id, component_id, start, end)
|