pingera-cli 0.1.3__tar.gz → 0.1.5__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 (47) hide show
  1. {pingera_cli-0.1.3/pingera_cli.egg-info → pingera_cli-0.1.5}/PKG-INFO +22 -3
  2. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/README.md +21 -2
  3. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/docs/cli-commands.md +65 -15
  4. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/__init__.py +1 -1
  5. pingera_cli-0.1.5/pingera_cli/commands/check_secrets.py +298 -0
  6. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/commands/checks.py +6 -2
  7. pingera_cli-0.1.5/pingera_cli/commands/secrets.py +316 -0
  8. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/main.py +2 -0
  9. {pingera_cli-0.1.3 → pingera_cli-0.1.5/pingera_cli.egg-info}/PKG-INFO +22 -3
  10. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli.egg-info/SOURCES.txt +2 -0
  11. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli.egg-info/requires.txt +1 -1
  12. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pyproject.toml +1 -1
  13. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/.gitignore +0 -0
  14. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/.replit +0 -0
  15. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/LICENSE +0 -0
  16. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/assert +0 -0
  17. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/__main__.py +0 -0
  18. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/commands/__init__.py +0 -0
  19. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/commands/auth.py +0 -0
  20. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/commands/base.py +0 -0
  21. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/commands/check_groups.py +0 -0
  22. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/commands/on_demand_checks.py +0 -0
  23. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/__init__.py +0 -0
  24. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/base_formatter.py +0 -0
  25. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/generic_formatter.py +0 -0
  26. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/multistep_formatter.py +0 -0
  27. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/registry.py +0 -0
  28. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/ssl_formatter.py +0 -0
  29. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/synthetic_formatter.py +0 -0
  30. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/formatters/web_formatter.py +0 -0
  31. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/utils/__init__.py +0 -0
  32. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/utils/config.py +0 -0
  33. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli/utils/console.py +0 -0
  34. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli.egg-info/dependency_links.txt +0 -0
  35. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli.egg-info/entry_points.txt +0 -0
  36. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/pingera_cli.egg-info/top_level.txt +0 -0
  37. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/replit.md +0 -0
  38. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/run_tests.py +0 -0
  39. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/setup.cfg +0 -0
  40. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/setup.py +0 -0
  41. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/tests/__init__.py +0 -0
  42. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/tests/conftest.py +0 -0
  43. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/tests/requirements.txt +0 -0
  44. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/tests/test_auth.py +0 -0
  45. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/tests/test_checks.py +0 -0
  46. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/tests/test_config.py +0 -0
  47. {pingera_cli-0.1.3 → pingera_cli-0.1.5}/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
3
+ Version: 0.1.5
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.5
34
+ Requires-Dist: pingera-sdk>=1.0.6
35
35
  Requires-Dist: click>=8.0.0
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: pytest>=6.0; extra == "dev"
@@ -44,7 +44,6 @@ Dynamic: home-page
44
44
  Dynamic: license-file
45
45
  Dynamic: requires-python
46
46
 
47
-
48
47
  # Pingera Platform CLI 🚀
49
48
 
50
49
  A beautiful Python CLI tool for the [Pingera Platform](https://pingera.ru) - built with typer and rich, distributed via pip and based on Pingera SDK.
@@ -61,6 +60,7 @@ A beautiful Python CLI tool for the [Pingera Platform](https://pingera.ru) - bui
61
60
  - **Cross-Platform**: Works on Windows, macOS, and Linux
62
61
  - **Easy Installation**: Simple pip install with all dependencies managed
63
62
  - **Configuration Management**: Flexible configuration with environment variables and config files
63
+ - **Secrets Management**: Manage organization secrets for monitoring checks
64
64
 
65
65
  ## 🚀 Installation
66
66
 
@@ -136,6 +136,25 @@ pngr checks groups create \
136
136
 
137
137
  # Assign a check to a group
138
138
  pngr checks assign-group check_123 --group-id group_456
139
+
140
+ # Remove a check from its group
141
+ pngr checks assign-group check_123 --group-id null
142
+ ```
143
+
144
+ ### Secrets Management
145
+
146
+ ```bash
147
+ # List organization secrets
148
+ pngr secrets list
149
+
150
+ # Create a new secret
151
+ pngr secrets create "DATABASE_PASSWORD"
152
+
153
+ # Update a secret's value
154
+ pngr secrets update sec123abc456
155
+
156
+ # Delete a secret
157
+ pngr secrets delete sec123abc456
139
158
  ```
140
159
 
141
160
  ### List available regions
@@ -1,4 +1,3 @@
1
-
2
1
  # Pingera Platform CLI 🚀
3
2
 
4
3
  A beautiful Python CLI tool for the [Pingera Platform](https://pingera.ru) - built with typer and rich, distributed via pip and based on Pingera SDK.
@@ -15,6 +14,7 @@ A beautiful Python CLI tool for the [Pingera Platform](https://pingera.ru) - bui
15
14
  - **Cross-Platform**: Works on Windows, macOS, and Linux
16
15
  - **Easy Installation**: Simple pip install with all dependencies managed
17
16
  - **Configuration Management**: Flexible configuration with environment variables and config files
17
+ - **Secrets Management**: Manage organization secrets for monitoring checks
18
18
 
19
19
  ## 🚀 Installation
20
20
 
@@ -90,6 +90,25 @@ pngr checks groups create \
90
90
 
91
91
  # Assign a check to a group
92
92
  pngr checks assign-group check_123 --group-id group_456
93
+
94
+ # Remove a check from its group
95
+ pngr checks assign-group check_123 --group-id null
96
+ ```
97
+
98
+ ### Secrets Management
99
+
100
+ ```bash
101
+ # List organization secrets
102
+ pngr secrets list
103
+
104
+ # Create a new secret
105
+ pngr secrets create "DATABASE_PASSWORD"
106
+
107
+ # Update a secret's value
108
+ pngr secrets update sec123abc456
109
+
110
+ # Delete a secret
111
+ pngr secrets delete sec123abc456
93
112
  ```
94
113
 
95
114
  ### List available regions
@@ -165,4 +184,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
165
184
 
166
185
  - **Issues**: [GitHub Issues](https://github.com/pingera/pingera-cli/issues)
167
186
  - **Documentation**: [Pingera CLI Docs](https://docs.pingera.ru/devs/cli)
168
- - **Platform Support**: [app.pingera.ru](https://app.pingera.ru)
187
+ - **Platform Support**: [app.pingera.ru](https://app.pingera.ru)
@@ -1,4 +1,3 @@
1
-
2
1
  # Pingera CLI Commands Specification
3
2
 
4
3
  ## Overview
@@ -70,23 +69,40 @@ pngr checks jobs get <job-id>
70
69
  Manage check groups for organizing checks.
71
70
 
72
71
  ```bash
73
- # List check groups
74
- pngr checks groups list [--page <num>] [--page-size <size>]
72
+ # List all check groups
73
+ pngr checks groups list
75
74
 
76
- # Get specific group
77
- pngr checks groups get <group-id>
75
+ # Get a specific check group
76
+ pngr checks groups get <group_id>
78
77
 
79
- # Create group
80
- pngr checks groups create --name <name> [--description <desc>] [--color <hex>] [--position <num>] [--active/--inactive]
78
+ # Create a new check group
79
+ pngr checks groups create "API Endpoints" --description "All API endpoint checks" --color "#4F46E5"
81
80
 
82
- # Update group
83
- pngr checks groups update <group-id> [--name <name>] [--description <desc>] [--color <hex>] [--position <num>] [--active/--inactive]
81
+ # Update a check group
82
+ pngr checks groups update <group_id> --name "Updated Name" --description "Updated description"
84
83
 
85
- # Delete group
86
- pngr checks groups delete <group-id> [--confirm]
84
+ # Delete a check group
85
+ pngr checks groups delete <group_id>
86
+ ```
87
+
88
+ ### `pngr checks secrets`
89
+ Manage secret associations for checks to inject environment variables.
87
90
 
88
- # Get checks in a group
89
- pngr checks groups checks <group-id> [--page <num>] [--page-size <size>]
91
+ ```bash
92
+ # List all secrets associated with a check
93
+ pngr checks secrets list <check_id>
94
+
95
+ # Add a secret to a check with environment variable name
96
+ pngr checks secrets add <check_id> <secret_id> DATABASE_PASSWORD
97
+
98
+ # Remove a secret association from a check
99
+ pngr checks secrets remove <check_id> <secret_id>
100
+
101
+ # Remove a secret without confirmation
102
+ pngr checks secrets remove <check_id> <secret_id> --force
103
+
104
+ # Replace all secret associations for a check
105
+ pngr checks secrets update-all <check_id> --associations '[{"secret_id": "sec123", "env_variable": "DB_PASS"}, {"secret_id": "sec456", "env_variable": "API_KEY"}]'
90
106
  ```
91
107
 
92
108
  ## On-Demand Checks Commands
@@ -257,6 +273,41 @@ pngr incidents updates update <incident-id> <update-id> --body <text> [--page-id
257
273
  pngr incidents updates delete <incident-id> <update-id> [--page-id <id>] [--confirm]
258
274
  ```
259
275
 
276
+ ## Secrets Management
277
+
278
+ Manage organization secrets for use in monitoring checks:
279
+
280
+ ```bash
281
+ # List all secrets
282
+ pngr secrets list
283
+
284
+ # List secrets with pagination
285
+ pngr secrets list --page 2 --page-size 50
286
+
287
+ # Get a specific secret by ID
288
+ pngr secrets get sec123abc456
289
+
290
+ # Create a new secret (will prompt for value securely)
291
+ pngr secrets create "DATABASE_PASSWORD"
292
+
293
+ # Create a secret with value provided via option
294
+ pngr secrets create "API_KEY" --value "your-secret-value"
295
+
296
+ # Update a secret's value (will prompt for new value securely)
297
+ pngr secrets update sec123abc456
298
+
299
+ # Update a secret with value provided via option
300
+ pngr secrets update sec123abc456 --value "new-secret-value"
301
+
302
+ # Delete a secret (with confirmation)
303
+ pngr secrets delete sec123abc456
304
+
305
+ # Delete a secret without confirmation
306
+ pngr secrets delete sec123abc456 --force
307
+ ```
308
+
309
+ **Note:** Secret values are hidden in list views for security. Only individual secret retrieval shows the actual value (when permissions allow).
310
+
260
311
  ## Configuration Commands
261
312
 
262
313
  ### `pngr config`
@@ -418,5 +469,4 @@ pngr checks assign-group check_123 --group-id group_456
418
469
  pngr checks assign-group check_123 --group-id null
419
470
 
420
471
  # Export incidents as JSON
421
- pngr incidents list --output json > incidents.json
422
- ```
472
+ pngr incidents list --output json > incidents.json
@@ -2,7 +2,7 @@
2
2
  Pingera CLI - A beautiful CLI tool for Pingera Platform
3
3
  """
4
4
 
5
- __version__ = "0.1.3"
5
+ __version__ = "0.1.5"
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,298 @@
1
+
2
+ """
3
+ Commands for managing check-secret associations
4
+ """
5
+
6
+ import os
7
+ from typing import Optional, List
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, get_output_format
16
+ from ..utils.console import console
17
+
18
+
19
+ class CheckSecretsCommand(BaseCommand):
20
+ """
21
+ Commands for managing check-secret associations
22
+ """
23
+
24
+ def __init__(self, output_format: Optional[str] = None):
25
+ super().__init__(output_format)
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 CheckSecretsApi
37
+
38
+ # Configure the client
39
+ configuration = Configuration()
40
+ configuration.host = "https://api.pingera.ru"
41
+ configuration.api_key['apiKeyAuth'] = api_key
42
+
43
+ # Create API client
44
+ api_client = ApiClient(configuration)
45
+ return CheckSecretsApi(api_client)
46
+ except ImportError:
47
+ self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
48
+ raise typer.Exit(1)
49
+ except Exception as e:
50
+ self.display_error(f"Failed to initialize client: {str(e)}")
51
+ raise typer.Exit(1)
52
+
53
+ def list_check_secrets(self, check_id: str):
54
+ """List all secrets associated with a check"""
55
+ try:
56
+ check_secrets_api = self.get_client()
57
+
58
+ # Make API call
59
+ secrets = check_secrets_api.v1_checks_check_id_secrets_get(check_id)
60
+
61
+ # Handle different output formats
62
+ if self.output_format in ['json', 'yaml']:
63
+ # Convert to dict for JSON/YAML output
64
+ secrets_data = [secret.to_dict() for secret in secrets]
65
+ self.output_data({
66
+ 'check_id': check_id,
67
+ 'secrets': secrets_data,
68
+ 'total': len(secrets_data)
69
+ })
70
+ else:
71
+ # Table format
72
+ if not secrets:
73
+ self.display_info(f"No secrets associated with check {check_id}")
74
+ return
75
+
76
+ table = Table(title=f"Secrets for Check {check_id}")
77
+ table.add_column("Association ID", style="cyan")
78
+ table.add_column("Secret ID", style="yellow")
79
+ table.add_column("Secret Name", style="white")
80
+ table.add_column("Environment Variable", style="green")
81
+ table.add_column("Created At", style="dim")
82
+
83
+ for secret in secrets:
84
+ secret_name = secret.secret.secret_name if hasattr(secret, 'secret') and secret.secret else 'Unknown'
85
+
86
+ table.add_row(
87
+ secret.id or '',
88
+ secret.secret_id or '',
89
+ secret_name,
90
+ secret.env_variable or '',
91
+ str(secret.created_at) if secret.created_at else ''
92
+ )
93
+
94
+ console.print(table)
95
+ console.print(f"\n[dim]Found {len(secrets)} secret associations[/dim]")
96
+
97
+ except Exception as e:
98
+ self.display_error(f"Failed to list check secrets: {str(e)}")
99
+ raise typer.Exit(1)
100
+
101
+ def add_secret_to_check(self, check_id: str, secret_id: str, env_variable: str):
102
+ """Add a secret to a check with an environment variable name"""
103
+ try:
104
+ check_secrets_api = self.get_client()
105
+
106
+ # Import the CheckSecret model
107
+ from pingera.models import CheckSecret
108
+
109
+ # Create the association
110
+ check_secret = CheckSecret(
111
+ secret_id=secret_id,
112
+ env_variable=env_variable
113
+ )
114
+
115
+ # Make API call
116
+ created_association = check_secrets_api.v1_checks_check_id_secrets_post(check_id, check_secret)
117
+
118
+ # Handle different output formats
119
+ if self.output_format in ['json', 'yaml']:
120
+ self.output_data(created_association.to_dict())
121
+ else:
122
+ self.display_success(
123
+ f"Secret '{secret_id}' successfully associated with check '{check_id}' as environment variable '{env_variable}'"
124
+ )
125
+
126
+ # Show details table
127
+ table = Table(title="Created Association")
128
+ table.add_column("Property", style="cyan")
129
+ table.add_column("Value", style="white")
130
+
131
+ table.add_row("Association ID", created_association.id or '')
132
+ table.add_row("Check ID", check_id)
133
+ table.add_row("Secret ID", created_association.secret_id or '')
134
+ table.add_row("Environment Variable", created_association.env_variable or '')
135
+ table.add_row("Created At", str(created_association.created_at) if created_association.created_at else '')
136
+
137
+ console.print(table)
138
+
139
+ except Exception as e:
140
+ self.display_error(f"Failed to add secret to check: {str(e)}")
141
+ raise typer.Exit(1)
142
+
143
+ def remove_secret_from_check(self, check_id: str, secret_id: str, force: bool = False):
144
+ """Remove a secret association from a check"""
145
+ try:
146
+ check_secrets_api = self.get_client()
147
+
148
+ # Confirmation prompt unless force is specified
149
+ if not force:
150
+ if not self.prompt_confirmation(
151
+ f"Are you sure you want to remove secret '{secret_id}' from check '{check_id}'?",
152
+ default=False
153
+ ):
154
+ self.display_info("Operation cancelled")
155
+ return
156
+
157
+ # Make API call
158
+ check_secrets_api.v1_checks_check_id_secrets_secret_id_delete(check_id, secret_id)
159
+
160
+ if self.output_format in ['json', 'yaml']:
161
+ self.output_data({
162
+ "message": f"Secret '{secret_id}' removed from check '{check_id}'",
163
+ "check_id": check_id,
164
+ "secret_id": secret_id
165
+ })
166
+ else:
167
+ self.display_success(f"Secret '{secret_id}' removed from check '{check_id}'")
168
+
169
+ except Exception as e:
170
+ self.display_error(f"Failed to remove secret from check: {str(e)}")
171
+ raise typer.Exit(1)
172
+
173
+ def update_all_secrets_for_check(self, check_id: str, associations: List[dict]):
174
+ """Replace all secret associations for a check"""
175
+ try:
176
+ check_secrets_api = self.get_client()
177
+
178
+ # Import the CheckSecret model
179
+ from pingera.models import CheckSecret
180
+
181
+ # Create CheckSecret objects from the associations
182
+ check_secrets = []
183
+ for assoc in associations:
184
+ check_secret = CheckSecret(
185
+ secret_id=assoc['secret_id'],
186
+ env_variable=assoc['env_variable']
187
+ )
188
+ check_secrets.append(check_secret)
189
+
190
+ # Make API call
191
+ updated_associations = check_secrets_api.v1_checks_check_id_secrets_put(check_id, check_secrets)
192
+
193
+ # Handle different output formats
194
+ if self.output_format in ['json', 'yaml']:
195
+ associations_data = [assoc.to_dict() for assoc in updated_associations]
196
+ self.output_data({
197
+ 'check_id': check_id,
198
+ 'secrets': associations_data,
199
+ 'total': len(associations_data)
200
+ })
201
+ else:
202
+ self.display_success(f"Updated all secret associations for check '{check_id}'")
203
+
204
+ if updated_associations:
205
+ table = Table(title=f"Updated Associations for Check {check_id}")
206
+ table.add_column("Association ID", style="cyan")
207
+ table.add_column("Secret ID", style="yellow")
208
+ table.add_column("Environment Variable", style="green")
209
+
210
+ for assoc in updated_associations:
211
+ table.add_row(
212
+ assoc.id or '',
213
+ assoc.secret_id or '',
214
+ assoc.env_variable or ''
215
+ )
216
+
217
+ console.print(table)
218
+ console.print(f"\n[dim]Total associations: {len(updated_associations)}[/dim]")
219
+ else:
220
+ console.print("[dim]No associations remaining[/dim]")
221
+
222
+ except Exception as e:
223
+ self.display_error(f"Failed to update check secrets: {str(e)}")
224
+ raise typer.Exit(1)
225
+
226
+
227
+ # Create Typer app for check secrets commands
228
+ app = typer.Typer(name="secrets", help="Manage secrets for checks")
229
+
230
+
231
+ @app.command("list")
232
+ def list_secrets(
233
+ check_id: str = typer.Argument(..., help="Check ID to list secrets for"),
234
+ ):
235
+ """List all secrets associated with a check"""
236
+ check_secrets_cmd = CheckSecretsCommand(get_output_format())
237
+ check_secrets_cmd.list_check_secrets(check_id)
238
+
239
+
240
+ @app.command("add")
241
+ def add_secret(
242
+ check_id: str = typer.Argument(..., help="Check ID to add secret to"),
243
+ secret_id: str = typer.Argument(..., help="Secret ID to associate"),
244
+ env_variable: str = typer.Argument(..., help="Environment variable name for the secret"),
245
+ ):
246
+ """Add a secret to a check with an environment variable name"""
247
+ if len(env_variable) > 100:
248
+ console.print("[red]Error:[/red] Environment variable name must be 100 characters or less")
249
+ raise typer.Exit(1)
250
+
251
+ check_secrets_cmd = CheckSecretsCommand(get_output_format())
252
+ check_secrets_cmd.add_secret_to_check(check_id, secret_id, env_variable)
253
+
254
+
255
+ @app.command("remove")
256
+ def remove_secret(
257
+ check_id: str = typer.Argument(..., help="Check ID to remove secret from"),
258
+ secret_id: str = typer.Argument(..., help="Secret ID to remove"),
259
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
260
+ ):
261
+ """Remove a secret association from a check"""
262
+ check_secrets_cmd = CheckSecretsCommand(get_output_format())
263
+ check_secrets_cmd.remove_secret_from_check(check_id, secret_id, force)
264
+
265
+
266
+ @app.command("update-all")
267
+ def update_all_secrets(
268
+ check_id: str = typer.Argument(..., help="Check ID to update secrets for"),
269
+ associations_json: str = typer.Option(..., "--associations", help="JSON array of secret associations: '[{\"secret_id\": \"sec123\", \"env_variable\": \"VAR_NAME\"}]'"),
270
+ ):
271
+ """Replace all secret associations for a check"""
272
+ import json
273
+
274
+ try:
275
+ associations = json.loads(associations_json)
276
+ if not isinstance(associations, list):
277
+ console.print("[red]Error:[/red] Associations must be a JSON array")
278
+ raise typer.Exit(1)
279
+
280
+ # Validate each association
281
+ for i, assoc in enumerate(associations):
282
+ if not isinstance(assoc, dict):
283
+ console.print(f"[red]Error:[/red] Association {i+1} must be an object")
284
+ raise typer.Exit(1)
285
+ if 'secret_id' not in assoc or 'env_variable' not in assoc:
286
+ console.print(f"[red]Error:[/red] Association {i+1} must have 'secret_id' and 'env_variable' fields")
287
+ raise typer.Exit(1)
288
+ if len(assoc['env_variable']) > 100:
289
+ console.print(f"[red]Error:[/red] Environment variable name in association {i+1} must be 100 characters or less")
290
+ raise typer.Exit(1)
291
+
292
+ except json.JSONDecodeError as e:
293
+ console.print(f"[red]Error:[/red] Invalid JSON: {str(e)}")
294
+ console.print("[dim]Example: '[{\"secret_id\": \"sec123abc456\", \"env_variable\": \"DATABASE_PASSWORD\"}]'[/dim]")
295
+ raise typer.Exit(1)
296
+
297
+ check_secrets_cmd = CheckSecretsCommand(get_output_format())
298
+ check_secrets_cmd.update_all_secrets_for_check(check_id, associations)
@@ -1183,7 +1183,11 @@ from .on_demand_checks import run_app, jobs_app
1183
1183
  # Import check groups functionality
1184
1184
  from .check_groups import app as groups_app
1185
1185
 
1186
- # Add the run, jobs, and groups subcommands to the main checks app
1186
+ # Import check secrets functionality
1187
+ from .check_secrets import app as check_secrets_app
1188
+
1189
+ # Add the run, jobs, groups, and secrets subcommands to the main checks app
1187
1190
  app.add_typer(run_app, name="run")
1188
1191
  app.add_typer(jobs_app, name="jobs")
1189
- app.add_typer(groups_app, name="groups")
1192
+ app.add_typer(groups_app, name="groups")
1193
+ app.add_typer(check_secrets_app, name="secrets")
@@ -0,0 +1,316 @@
1
+
2
+ """
3
+ Commands for managing organization secrets
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ from typing import Optional
9
+
10
+ import typer
11
+ from rich.table import Table
12
+
13
+ from .base import BaseCommand
14
+ from ..utils.config import get_api_key, get_output_format
15
+ from ..utils.console import console
16
+
17
+
18
+ class SecretsCommand(BaseCommand):
19
+ """
20
+ Commands for managing organization secrets
21
+ """
22
+
23
+ def __init__(self, output_format: Optional[str] = None):
24
+ super().__init__(output_format)
25
+
26
+ def get_client(self):
27
+ """Get Pingera SDK client with authentication"""
28
+ api_key = get_api_key()
29
+ if not api_key:
30
+ self.display_error("API key not found. Use 'pngr auth login --api-key <key>' to set it.")
31
+ raise typer.Exit(1)
32
+
33
+ try:
34
+ from pingera import ApiClient, Configuration
35
+ from pingera.api import SecretsApi
36
+
37
+ # Configure the client
38
+ configuration = Configuration()
39
+ configuration.host = "https://api.pingera.ru"
40
+ configuration.api_key['apiKeyAuth'] = api_key
41
+
42
+ # Create API client
43
+ api_client = ApiClient(configuration)
44
+ return SecretsApi(api_client)
45
+ except ImportError:
46
+ self.display_error("Pingera SDK not installed. Install with: pip install pingera-sdk")
47
+ raise typer.Exit(1)
48
+ except Exception as e:
49
+ self.display_error(f"Failed to initialize client: {str(e)}")
50
+ raise typer.Exit(1)
51
+
52
+ def list_secrets(self, page: int = 1, page_size: int = 20):
53
+ """List organization secrets"""
54
+ try:
55
+ secrets_api = self.get_client()
56
+
57
+ # Make API call
58
+ response = secrets_api.v1_secrets_get(page=page, page_size=page_size)
59
+
60
+ # Handle named tuple/object response with secrets and pagination attributes
61
+ if hasattr(response, 'secrets'):
62
+ secrets = response.secrets
63
+ pagination = getattr(response, 'pagination', {})
64
+ elif isinstance(response, (list, tuple)) and len(response) >= 2:
65
+ secrets = response[0]
66
+ pagination = response[1] if len(response) > 1 else {}
67
+ else:
68
+ secrets = response
69
+ pagination = {}
70
+
71
+ # Handle different output formats
72
+ if self.output_format in ['json', 'yaml']:
73
+ # Convert to dict for JSON/YAML output
74
+ secrets_data = {
75
+ 'secrets': [secret.to_dict() for secret in secrets],
76
+ 'pagination': pagination if pagination else {
77
+ 'page': page,
78
+ 'page_size': page_size,
79
+ 'total': len(secrets)
80
+ }
81
+ }
82
+ self.output_data(secrets_data)
83
+ else:
84
+ # Table format
85
+ if not secrets:
86
+ self.display_info("No secrets found")
87
+ return
88
+
89
+ table = Table(title="Organization Secrets")
90
+ table.add_column("ID", style="cyan")
91
+ table.add_column("Name", style="white")
92
+ table.add_column("Created At", style="dim")
93
+ table.add_column("Updated At", style="dim")
94
+ table.add_column("Created By", style="dim")
95
+
96
+ for secret in secrets:
97
+ table.add_row(
98
+ secret.id or '',
99
+ secret.secret_name or '',
100
+ str(secret.created_at) if secret.created_at else '',
101
+ str(secret.updated_at) if secret.updated_at else '',
102
+ secret.created_by or ''
103
+ )
104
+
105
+ console.print(table)
106
+
107
+ # Show pagination info
108
+ if pagination and pagination.get('total_pages', 1) > 1:
109
+ current_page = pagination.get('page', page)
110
+ total_pages = pagination.get('total_pages', 1)
111
+ total_items = pagination.get('total_items', len(secrets))
112
+ console.print(f"\n[dim]Page {current_page} of {total_pages} ({total_items} total secrets)[/dim]")
113
+ else:
114
+ console.print(f"\n[dim]Found {len(secrets)} secrets[/dim]")
115
+
116
+ except Exception as e:
117
+ self.display_error(f"Failed to list secrets: {str(e)}")
118
+ raise typer.Exit(1)
119
+
120
+ def get_secret(self, secret_id: str):
121
+ """Get a specific secret by ID"""
122
+ try:
123
+ secrets_api = self.get_client()
124
+
125
+ # Make API call
126
+ secret = secrets_api.v1_secrets_secret_id_get(secret_id)
127
+
128
+ # Handle different output formats
129
+ if self.output_format in ['json', 'yaml']:
130
+ self.output_data(secret.to_dict())
131
+ else:
132
+ # Table format
133
+ table = Table(title=f"Secret Details: {secret.secret_name}")
134
+ table.add_column("Property", style="cyan")
135
+ table.add_column("Value", style="white")
136
+
137
+ table.add_row("ID", secret.id or '')
138
+ table.add_row("Name", secret.secret_name or '')
139
+
140
+ # Show secret value if available (marked as sensitive)
141
+ if hasattr(secret, 'secret_value') and secret.secret_value:
142
+ table.add_row("Value", f"[red]{'*' * 20}[/red] (hidden for security)")
143
+
144
+ table.add_row("Created At", str(secret.created_at) if secret.created_at else '')
145
+ table.add_row("Updated At", str(secret.updated_at) if secret.updated_at else '')
146
+ table.add_row("Created By", secret.created_by or '')
147
+ table.add_row("Updated By", secret.updated_by or '')
148
+
149
+ console.print(table)
150
+
151
+ except Exception as e:
152
+ self.display_error(f"Failed to get secret: {str(e)}")
153
+ raise typer.Exit(1)
154
+
155
+ def create_secret(self, name: str, value: str):
156
+ """Create a new secret"""
157
+ try:
158
+ secrets_api = self.get_client()
159
+
160
+ # Import the Secret model
161
+ from pingera.models import Secret1
162
+
163
+ # Create secret object
164
+ secret_data = Secret1(
165
+ secret_name=name,
166
+ secret_value=value
167
+ )
168
+
169
+ # Make API call
170
+ created_secret = secrets_api.v1_secrets_post(secret_data)
171
+
172
+ # Handle different output formats
173
+ if self.output_format in ['json', 'yaml']:
174
+ self.output_data(created_secret.to_dict())
175
+ else:
176
+ self.display_success(f"Secret '{name}' created successfully with ID: {created_secret.id}")
177
+
178
+ # Show details table
179
+ table = Table(title="Created Secret")
180
+ table.add_column("Property", style="cyan")
181
+ table.add_column("Value", style="white")
182
+
183
+ table.add_row("ID", created_secret.id or '')
184
+ table.add_row("Name", created_secret.secret_name or '')
185
+ table.add_row("Created At", str(created_secret.created_at) if created_secret.created_at else '')
186
+
187
+ console.print(table)
188
+
189
+ except Exception as e:
190
+ self.display_error(f"Failed to create secret: {str(e)}")
191
+ raise typer.Exit(1)
192
+
193
+ def update_secret(self, secret_id: str, value: str):
194
+ """Update an existing secret"""
195
+ try:
196
+ secrets_api = self.get_client()
197
+
198
+ # Import the Secret model for update
199
+ from pingera.models import Secret2
200
+
201
+ # Create update object (only value can be updated)
202
+ secret_update = Secret2(secret_value=value)
203
+
204
+ # Make API call
205
+ updated_secret = secrets_api.v1_secrets_secret_id_patch(secret_id, secret_update)
206
+
207
+ # Handle different output formats
208
+ if self.output_format in ['json', 'yaml']:
209
+ self.output_data(updated_secret.to_dict())
210
+ else:
211
+ self.display_success(f"Secret '{updated_secret.secret_name}' updated successfully")
212
+
213
+ # Show details table
214
+ table = Table(title="Updated Secret")
215
+ table.add_column("Property", style="cyan")
216
+ table.add_column("Value", style="white")
217
+
218
+ table.add_row("ID", updated_secret.id or '')
219
+ table.add_row("Name", updated_secret.secret_name or '')
220
+ table.add_row("Updated At", str(updated_secret.updated_at) if updated_secret.updated_at else '')
221
+
222
+ console.print(table)
223
+
224
+ except Exception as e:
225
+ self.display_error(f"Failed to update secret: {str(e)}")
226
+ raise typer.Exit(1)
227
+
228
+ def delete_secret(self, secret_id: str, force: bool = False):
229
+ """Delete a secret"""
230
+ try:
231
+ secrets_api = self.get_client()
232
+
233
+ # Get secret details first for confirmation
234
+ try:
235
+ secret = secrets_api.v1_secrets_secret_id_get(secret_id)
236
+ secret_name = secret.secret_name
237
+ except Exception:
238
+ secret_name = secret_id
239
+
240
+ # Confirmation prompt unless force is specified
241
+ if not force:
242
+ if not self.prompt_confirmation(
243
+ f"Are you sure you want to delete secret '{secret_name}'? This action cannot be undone.",
244
+ default=False
245
+ ):
246
+ self.display_info("Operation cancelled")
247
+ return
248
+
249
+ # Make API call
250
+ secrets_api.v1_secrets_secret_id_delete(secret_id)
251
+
252
+ if self.output_format in ['json', 'yaml']:
253
+ self.output_data({"message": f"Secret '{secret_name}' deleted successfully", "secret_id": secret_id})
254
+ else:
255
+ self.display_success(f"Secret '{secret_name}' deleted successfully")
256
+
257
+ except Exception as e:
258
+ self.display_error(f"Failed to delete secret: {str(e)}")
259
+ raise typer.Exit(1)
260
+
261
+
262
+ # Create Typer app for secrets commands
263
+ app = typer.Typer(name="secrets", help="Manage organization secrets")
264
+
265
+
266
+ @app.command("list")
267
+ def list_secrets(
268
+ page: int = typer.Option(1, "--page", "-p", help="Page number (default: 1)"),
269
+ page_size: int = typer.Option(20, "--page-size", "-s", help="Number of items per page (default: 20, max: 100)"),
270
+ ):
271
+ """List all organization secrets"""
272
+ secrets_cmd = SecretsCommand(get_output_format())
273
+ secrets_cmd.list_secrets(page, page_size)
274
+
275
+
276
+ @app.command("get")
277
+ def get_secret(
278
+ secret_id: str = typer.Argument(..., help="Secret ID to retrieve"),
279
+ ):
280
+ """Get a specific secret by ID"""
281
+ secrets_cmd = SecretsCommand(get_output_format())
282
+ secrets_cmd.get_secret(secret_id)
283
+
284
+
285
+ @app.command("create")
286
+ def create_secret(
287
+ name: str = typer.Argument(..., help="Secret name (max 100 characters)"),
288
+ value: str = typer.Option(..., "--value", "-v", help="Secret value", prompt=True, hide_input=True),
289
+ ):
290
+ """Create a new secret"""
291
+ if len(name) > 100:
292
+ console.print("[red]Error:[/red] Secret name must be 100 characters or less")
293
+ raise typer.Exit(1)
294
+
295
+ secrets_cmd = SecretsCommand(get_output_format())
296
+ secrets_cmd.create_secret(name, value)
297
+
298
+
299
+ @app.command("update")
300
+ def update_secret(
301
+ secret_id: str = typer.Argument(..., help="Secret ID to update"),
302
+ value: str = typer.Option(..., "--value", "-v", help="New secret value", prompt=True, hide_input=True),
303
+ ):
304
+ """Update an existing secret (only the value can be updated)"""
305
+ secrets_cmd = SecretsCommand(get_output_format())
306
+ secrets_cmd.update_secret(secret_id, value)
307
+
308
+
309
+ @app.command("delete")
310
+ def delete_secret(
311
+ secret_id: str = typer.Argument(..., help="Secret ID to delete"),
312
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
313
+ ):
314
+ """Delete a secret (this action cannot be undone)"""
315
+ secrets_cmd = SecretsCommand(get_output_format())
316
+ secrets_cmd.delete_secret(secret_id, force)
@@ -15,6 +15,7 @@ from . import __version__
15
15
  from .commands.base import BaseCommand
16
16
  from .commands.auth import auth_cmd
17
17
  from .commands.checks import app as checks_app
18
+ from .commands.secrets import app as secrets_app
18
19
  from .utils.console import console, error_console
19
20
  from .utils.config import get_config
20
21
 
@@ -33,6 +34,7 @@ base_cmd = BaseCommand()
33
34
  # Add subcommands
34
35
  app.add_typer(auth_cmd.app, name="auth")
35
36
  app.add_typer(checks_app, name="checks")
37
+ app.add_typer(secrets_app, name="secrets")
36
38
 
37
39
 
38
40
  @app.command("version")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pingera-cli
3
- Version: 0.1.3
3
+ Version: 0.1.5
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.5
34
+ Requires-Dist: pingera-sdk>=1.0.6
35
35
  Requires-Dist: click>=8.0.0
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: pytest>=6.0; extra == "dev"
@@ -44,7 +44,6 @@ Dynamic: home-page
44
44
  Dynamic: license-file
45
45
  Dynamic: requires-python
46
46
 
47
-
48
47
  # Pingera Platform CLI 🚀
49
48
 
50
49
  A beautiful Python CLI tool for the [Pingera Platform](https://pingera.ru) - built with typer and rich, distributed via pip and based on Pingera SDK.
@@ -61,6 +60,7 @@ A beautiful Python CLI tool for the [Pingera Platform](https://pingera.ru) - bui
61
60
  - **Cross-Platform**: Works on Windows, macOS, and Linux
62
61
  - **Easy Installation**: Simple pip install with all dependencies managed
63
62
  - **Configuration Management**: Flexible configuration with environment variables and config files
63
+ - **Secrets Management**: Manage organization secrets for monitoring checks
64
64
 
65
65
  ## 🚀 Installation
66
66
 
@@ -136,6 +136,25 @@ pngr checks groups create \
136
136
 
137
137
  # Assign a check to a group
138
138
  pngr checks assign-group check_123 --group-id group_456
139
+
140
+ # Remove a check from its group
141
+ pngr checks assign-group check_123 --group-id null
142
+ ```
143
+
144
+ ### Secrets Management
145
+
146
+ ```bash
147
+ # List organization secrets
148
+ pngr secrets list
149
+
150
+ # Create a new secret
151
+ pngr secrets create "DATABASE_PASSWORD"
152
+
153
+ # Update a secret's value
154
+ pngr secrets update sec123abc456
155
+
156
+ # Delete a secret
157
+ pngr secrets delete sec123abc456
139
158
  ```
140
159
 
141
160
  ### List available regions
@@ -21,8 +21,10 @@ pingera_cli/commands/__init__.py
21
21
  pingera_cli/commands/auth.py
22
22
  pingera_cli/commands/base.py
23
23
  pingera_cli/commands/check_groups.py
24
+ pingera_cli/commands/check_secrets.py
24
25
  pingera_cli/commands/checks.py
25
26
  pingera_cli/commands/on_demand_checks.py
27
+ pingera_cli/commands/secrets.py
26
28
  pingera_cli/formatters/__init__.py
27
29
  pingera_cli/formatters/base_formatter.py
28
30
  pingera_cli/formatters/generic_formatter.py
@@ -1,6 +1,6 @@
1
1
  typer[all]>=0.9.0
2
2
  rich>=13.0.0
3
- pingera-sdk>=1.0.5
3
+ pingera-sdk>=1.0.6
4
4
  click>=8.0.0
5
5
 
6
6
  [dev]
@@ -35,7 +35,7 @@ requires-python = ">=3.7"
35
35
  dependencies = [
36
36
  "typer[all]>=0.9.0",
37
37
  "rich>=13.0.0",
38
- "pingera-sdk>=1.0.5",
38
+ "pingera-sdk>=1.0.6",
39
39
  "click>=8.0.0",
40
40
  ]
41
41
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes