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.
Files changed (59) hide show
  1. {pingera_cli-0.1.15/pingera_cli.egg-info → pingera_cli-0.1.16}/PKG-INFO +2 -2
  2. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/cli-commands.md +39 -21
  3. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/__init__.py +1 -1
  4. pingera_cli-0.1.16/pingera_cli/commands/components.py +539 -0
  5. pingera_cli-0.1.16/pingera_cli/commands/incidents.py +578 -0
  6. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/on_demand_checks.py +5 -0
  7. pingera_cli-0.1.16/pingera_cli/commands/pages.py +810 -0
  8. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/main.py +18 -15
  9. {pingera_cli-0.1.15 → pingera_cli-0.1.16/pingera_cli.egg-info}/PKG-INFO +2 -2
  10. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/SOURCES.txt +3 -0
  11. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/requires.txt +1 -1
  12. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pyproject.toml +1 -1
  13. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/.gitignore +0 -0
  14. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/.replit +0 -0
  15. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/LICENSE +0 -0
  16. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/README.md +0 -0
  17. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/assert +0 -0
  18. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/example-check.yaml +0 -0
  19. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/example-synthetic-screenshot.json +0 -0
  20. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/docs/example-web-check.json +0 -0
  21. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/__main__.py +0 -0
  22. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/__init__.py +0 -0
  23. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/auth.py +0 -0
  24. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/base.py +0 -0
  25. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/check_groups.py +0 -0
  26. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/check_secrets.py +0 -0
  27. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/checks.py +0 -0
  28. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/execution_groups.py +0 -0
  29. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/commands/secrets.py +0 -0
  30. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/__init__.py +0 -0
  31. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/base_formatter.py +0 -0
  32. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/dns_formatter.py +0 -0
  33. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/generic_formatter.py +0 -0
  34. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/icmp_formatter.py +0 -0
  35. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/multistep_formatter.py +0 -0
  36. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/portscan_formatter.py +0 -0
  37. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/registry.py +0 -0
  38. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/ssl_formatter.py +0 -0
  39. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/synthetic_formatter.py +0 -0
  40. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/formatters/web_formatter.py +0 -0
  41. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/__init__.py +0 -0
  42. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/config.py +0 -0
  43. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/console.py +0 -0
  44. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli/utils/file_utils.py +0 -0
  45. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/dependency_links.txt +0 -0
  46. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/entry_points.txt +0 -0
  47. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pingera_cli.egg-info/top_level.txt +0 -0
  48. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/pwscript.js +0 -0
  49. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/replit.md +0 -0
  50. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/run_tests.py +0 -0
  51. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/setup.cfg +0 -0
  52. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/setup.py +0 -0
  53. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/__init__.py +0 -0
  54. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/conftest.py +0 -0
  55. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/requirements.txt +0 -0
  56. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/test_auth.py +0 -0
  57. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/test_checks.py +0 -0
  58. {pingera_cli-0.1.15 → pingera_cli-0.1.16}/tests/test_config.py +0 -0
  59. {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.15
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.13
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 (soon)
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 <name> [--description <desc>] [--subdomain <sub>] [--theme <theme>]
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> --theme <theme>
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 [--page-id <id>] [--page <num>] [--page-size <size>]
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> [--page-id <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>] [--page-id <id>]
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> --page-id <id>
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> [--page-id <id>] [--confirm]
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> [--page-id <id>] [--from <date>] [--to <date>]
240
+ pngr pages components uptime <component-id> --page-id <page-id> [--start <date>] [--end <date>]
244
241
 
245
- # Bulk update component uptime
246
- pngr components uptime-bulk --file <json-file> [--page-id <id>]
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 status comp_123 --status degraded_performance
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.15"
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)