pragmatiks-cli 0.5.1__py3-none-any.whl

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.
@@ -0,0 +1,199 @@
1
+ """CLI commands for resource management with lifecycle operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ import yaml
11
+ from rich import print
12
+ from rich.console import Console
13
+ from rich.markup import escape
14
+
15
+ from pragma_cli import get_client
16
+ from pragma_cli.commands.completions import (
17
+ completion_resource_ids,
18
+ completion_resource_names,
19
+ )
20
+ from pragma_cli.helpers import parse_resource_id
21
+
22
+
23
+ console = Console()
24
+ app = typer.Typer()
25
+
26
+
27
+ def resolve_file_references(resource: dict, base_dir: Path) -> dict:
28
+ """Resolve file references in secret resource config.
29
+
30
+ For pragma/secret resources, scans config.data values for '@' prefix
31
+ and replaces them with the file contents.
32
+
33
+ Args:
34
+ resource: Resource dictionary from YAML.
35
+ base_dir: Base directory for resolving relative paths.
36
+
37
+ Returns:
38
+ Resource dictionary with file references resolved.
39
+
40
+ Raises:
41
+ typer.Exit: If a referenced file is not found.
42
+ """
43
+ is_secret = resource.get("provider") == "pragma" and resource.get("resource") == "secret"
44
+ if not is_secret:
45
+ return resource
46
+
47
+ config = resource.get("config")
48
+ if not config or not isinstance(config, dict):
49
+ return resource
50
+
51
+ data = config.get("data")
52
+ if not data or not isinstance(data, dict):
53
+ return resource
54
+
55
+ resolved_data = {}
56
+ for key, value in data.items():
57
+ if isinstance(value, str) and value.startswith("@"):
58
+ file_path = Path(value[1:])
59
+ if not file_path.is_absolute():
60
+ file_path = base_dir / file_path
61
+
62
+ if not file_path.exists():
63
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
64
+ raise typer.Exit(1)
65
+
66
+ try:
67
+ resolved_data[key] = file_path.read_text()
68
+ except OSError as e:
69
+ console.print(f"[red]Error:[/red] Cannot read file {file_path}: {e}")
70
+ raise typer.Exit(1)
71
+ else:
72
+ resolved_data[key] = value
73
+
74
+ resolved_resource = resource.copy()
75
+ resolved_resource["config"] = {**config, "data": resolved_data}
76
+ return resolved_resource
77
+
78
+
79
+ def format_state(state: str) -> str:
80
+ """Format lifecycle state for display, escaping Rich markup.
81
+
82
+ Returns:
83
+ State string wrapped in brackets and escaped for Rich console.
84
+ """
85
+ return escape(f"[{state}]")
86
+
87
+
88
+ @app.command("list")
89
+ def list_resources(
90
+ provider: Annotated[str | None, typer.Option("--provider", "-p", help="Filter by provider")] = None,
91
+ resource: Annotated[str | None, typer.Option("--resource", "-r", help="Filter by resource type")] = None,
92
+ tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Filter by tags")] = None,
93
+ ):
94
+ """List resources, optionally filtered by provider, resource type, or tags."""
95
+ client = get_client()
96
+ for res in client.list_resources(provider=provider, resource=resource, tags=tags):
97
+ print(f"{res['provider']}/{res['resource']}/{res['name']} {format_state(res['lifecycle_state'])}")
98
+
99
+
100
+ @app.command()
101
+ def get(
102
+ resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
103
+ name: Annotated[str | None, typer.Argument(autocompletion=completion_resource_names)] = None,
104
+ ):
105
+ """Get resources by provider/resource type, optionally filtered by name."""
106
+ client = get_client()
107
+ provider, resource = parse_resource_id(resource_id)
108
+ if name:
109
+ res = client.get_resource(provider=provider, resource=resource, name=name)
110
+ print(f"{resource_id}/{res['name']} {format_state(res['lifecycle_state'])}")
111
+ else:
112
+ for res in client.list_resources(provider=provider, resource=resource):
113
+ print(f"{resource_id}/{res['name']} {format_state(res['lifecycle_state'])}")
114
+
115
+
116
+ @app.command()
117
+ def apply(
118
+ file: list[typer.FileText],
119
+ pending: Annotated[
120
+ bool, typer.Option("--pending", "-p", help="Queue for processing (set lifecycle_state to PENDING)")
121
+ ] = False,
122
+ ):
123
+ """Apply resources from YAML files (multi-document supported).
124
+
125
+ By default, resources are created in DRAFT state (not processed).
126
+ Use --pending to queue for immediate processing.
127
+
128
+ For pragma/secret resources, file references in config.data values
129
+ are resolved before submission. Use '@path/to/file' syntax to inline
130
+ file contents.
131
+ """
132
+ client = get_client()
133
+ for f in file:
134
+ base_dir = Path(f.name).parent
135
+ resources = yaml.safe_load_all(f.read())
136
+
137
+ for resource in resources:
138
+ resource = resolve_file_references(resource, base_dir)
139
+ if pending:
140
+ resource["lifecycle_state"] = "pending"
141
+ result = client.apply_resource(resource=resource)
142
+ res_id = f"{result['provider']}/{result['resource']}/{result['name']}"
143
+ print(f"Applied {res_id} {format_state(result['lifecycle_state'])}")
144
+
145
+
146
+ @app.command()
147
+ def delete(
148
+ resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
149
+ name: Annotated[str, typer.Argument(autocompletion=completion_resource_names)],
150
+ ):
151
+ """Delete a resource."""
152
+ client = get_client()
153
+ provider, resource = parse_resource_id(resource_id)
154
+ client.delete_resource(provider=provider, resource=resource, name=name)
155
+ print(f"Deleted {resource_id}/{name}")
156
+
157
+
158
+ @app.command()
159
+ def register(
160
+ resource_id: Annotated[str, typer.Argument(help="Resource type in provider/resource format")],
161
+ description: Annotated[str | None, typer.Option("--description", "-d", help="Resource type description")] = None,
162
+ schema_file: Annotated[typer.FileText | None, typer.Option("--schema", "-s", help="JSON schema file")] = None,
163
+ tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Tags for categorization")] = None,
164
+ ):
165
+ """Register a new resource type.
166
+
167
+ Registers a resource type so that resources of this type can be created.
168
+ Providers use this to declare what resources they can manage.
169
+ """
170
+ client = get_client()
171
+ provider, resource = parse_resource_id(resource_id)
172
+
173
+ schema = None
174
+ if schema_file:
175
+ schema = json.load(schema_file)
176
+
177
+ client.register_resource(
178
+ provider=provider,
179
+ resource=resource,
180
+ schema=schema,
181
+ description=description,
182
+ tags=tags,
183
+ )
184
+ print(f"Registered {resource_id}")
185
+
186
+
187
+ @app.command()
188
+ def unregister(
189
+ resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
190
+ ):
191
+ """Unregister a resource type.
192
+
193
+ Removes a resource type registration. Existing resources of this type
194
+ will no longer be manageable.
195
+ """
196
+ client = get_client()
197
+ provider, resource = parse_resource_id(resource_id)
198
+ client.unregister_resource(provider=provider, resource=resource)
199
+ print(f"Unregistered {resource_id}")
pragma_cli/config.py ADDED
@@ -0,0 +1,86 @@
1
+ """CLI configuration management for contexts and credentials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+ from pydantic import BaseModel
10
+
11
+
12
+ def _get_config_dir() -> Path:
13
+ """Get the configuration directory following XDG Base Directory specification.
14
+
15
+ Returns:
16
+ Path to configuration directory (~/.config/pragma by default).
17
+ """
18
+ xdg_config_home = os.getenv("XDG_CONFIG_HOME")
19
+ if xdg_config_home:
20
+ return Path(xdg_config_home) / "pragma"
21
+ return Path.home() / ".config" / "pragma"
22
+
23
+
24
+ CONFIG_DIR = _get_config_dir()
25
+ CONFIG_PATH = CONFIG_DIR / "config"
26
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials"
27
+
28
+
29
+ class ContextConfig(BaseModel):
30
+ """Configuration for a single CLI context."""
31
+
32
+ api_url: str
33
+
34
+
35
+ class PragmaConfig(BaseModel):
36
+ """CLI configuration with multiple named contexts."""
37
+
38
+ current_context: str
39
+ contexts: dict[str, ContextConfig]
40
+
41
+
42
+ def load_config() -> PragmaConfig:
43
+ """Load config from ~/.config/pragma/config.
44
+
45
+ Returns:
46
+ PragmaConfig with contexts loaded from file, or default if not found.
47
+ """
48
+ if not CONFIG_PATH.exists():
49
+ return PragmaConfig(
50
+ current_context="default", contexts={"default": ContextConfig(api_url="https://api.pragmatiks.io")}
51
+ )
52
+
53
+ with open(CONFIG_PATH) as f:
54
+ data = yaml.safe_load(f)
55
+ return PragmaConfig.model_validate(data)
56
+
57
+
58
+ def save_config(config: PragmaConfig):
59
+ """Save config to ~/.config/pragma/config."""
60
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
61
+ with open(CONFIG_PATH, "w") as f:
62
+ yaml.safe_dump(config.model_dump(), f)
63
+ CONFIG_PATH.chmod(0o644)
64
+
65
+
66
+ def get_current_context(context_name: str | None = None) -> tuple[str, ContextConfig]:
67
+ """Get context name and configuration.
68
+
69
+ Args:
70
+ context_name: Explicit context name. If None, uses current context from config.
71
+
72
+ Returns:
73
+ Tuple of (context_name, context_config).
74
+
75
+ Raises:
76
+ ValueError: If context not found in configuration.
77
+ """
78
+ config = load_config()
79
+
80
+ if context_name is None:
81
+ context_name = config.current_context
82
+
83
+ if context_name not in config.contexts:
84
+ raise ValueError(f"Context '{context_name}' not found in configuration")
85
+
86
+ return context_name, config.contexts[context_name]
pragma_cli/helpers.py ADDED
@@ -0,0 +1,21 @@
1
+ """CLI helper functions for parsing resource identifiers."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def parse_resource_id(resource_id: str) -> tuple[str, str]:
7
+ """Parse resource identifier into provider and resource type.
8
+
9
+ Args:
10
+ resource_id: Resource identifier in format 'provider/resource'.
11
+
12
+ Returns:
13
+ Tuple of (provider, resource).
14
+
15
+ Raises:
16
+ ValueError: If resource_id format is invalid.
17
+ """
18
+ if "/" not in resource_id:
19
+ raise ValueError(f"Invalid resource ID format: {resource_id}. Expected 'provider/resource'.")
20
+ provider, resource = resource_id.split("/", 1)
21
+ return provider, resource
pragma_cli/main.py ADDED
@@ -0,0 +1,67 @@
1
+ """CLI entry point with Typer application setup and command routing."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from pragma_sdk import PragmaClient
7
+
8
+ from pragma_cli import set_client
9
+ from pragma_cli.commands import auth, config, ops, provider, resources
10
+ from pragma_cli.config import get_current_context
11
+
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ @app.callback()
17
+ def main(
18
+ ctx: typer.Context,
19
+ context: Annotated[
20
+ str | None,
21
+ typer.Option(
22
+ "--context",
23
+ "-c",
24
+ help="Configuration context to use",
25
+ envvar="PRAGMA_CONTEXT",
26
+ ),
27
+ ] = None,
28
+ token: Annotated[
29
+ str | None,
30
+ typer.Option(
31
+ "--token",
32
+ "-t",
33
+ help="Override authentication token (not recommended, use environment variable instead)",
34
+ ),
35
+ ] = None,
36
+ ):
37
+ """Pragma CLI - Declarative resource management.
38
+
39
+ Authentication (industry-standard pattern):
40
+ - CLI writes credentials: 'pragma login' stores tokens in ~/.config/pragma/credentials
41
+ - SDK reads credentials: Automatic token discovery via precedence chain
42
+
43
+ Token Discovery Precedence:
44
+ 1. --token flag (explicit override)
45
+ 2. PRAGMA_AUTH_TOKEN_<CONTEXT> context-specific environment variable
46
+ 3. PRAGMA_AUTH_TOKEN environment variable
47
+ 4. ~/.config/pragma/credentials file (from pragma login)
48
+ 5. No authentication
49
+ """
50
+ context_name, context_config = get_current_context(context)
51
+
52
+ if token:
53
+ client = PragmaClient(base_url=context_config.api_url, auth_token=token)
54
+ else:
55
+ client = PragmaClient(base_url=context_config.api_url, context=context_name, require_auth=False)
56
+
57
+ set_client(client)
58
+
59
+
60
+ app.add_typer(resources.app, name="resources")
61
+ app.add_typer(auth.app, name="auth")
62
+ app.add_typer(config.app, name="config")
63
+ app.add_typer(ops.app, name="ops")
64
+ app.add_typer(provider.app, name="provider")
65
+
66
+ if __name__ == "__main__": # pragma: no cover
67
+ app()
pragma_cli/py.typed ADDED
File without changes
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.3
2
+ Name: pragmatiks-cli
3
+ Version: 0.5.1
4
+ Summary: Command-line interface for Pragmatiks
5
+ Requires-Dist: typer>=0.15.3
6
+ Requires-Dist: pragmatiks-sdk>=0.6.0
7
+ Requires-Dist: pyyaml>=6.0.3
8
+ Requires-Dist: copier>=9.0.0
9
+ Requires-Dist: rich>=13.9.0
10
+ Requires-Python: >=3.13
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Pragmatiks CLI
14
+
15
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pragmatiks/cli)
16
+ [![PyPI version](https://img.shields.io/pypi/v/pragmatiks-cli.svg)](https://pypi.org/project/pragmatiks-cli/)
17
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
18
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
19
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
20
+
21
+ **[Documentation](https://docs.pragmatiks.io/cli/overview)** | **[SDK](https://github.com/pragmatiks/sdk)** | **[Providers](https://github.com/pragmatiks/providers)**
22
+
23
+ Command-line interface for managing Pragmatiks resources.
24
+
25
+ <!-- TODO: Add logo and demo GIF -->
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ # Authenticate
31
+ pragma auth login
32
+
33
+ # Apply a resource
34
+ pragma resources apply bucket.yaml
35
+
36
+ # Check status
37
+ pragma resources get gcp/storage my-bucket
38
+ ```
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install pragmatiks-cli
44
+ ```
45
+
46
+ Or with uv:
47
+
48
+ ```bash
49
+ uv add pragmatiks-cli
50
+ ```
51
+
52
+ Enable shell completion for intelligent command-line assistance:
53
+
54
+ ```bash
55
+ pragma --install-completion
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **Declarative Resources** - Apply, get, and delete resources with YAML manifests
61
+ - **Smart Completion** - Tab completion for providers, resources, and names
62
+ - **Provider Development** - Initialize, sync, and deploy custom providers
63
+ - **Multi-document Support** - Apply multiple resources from a single YAML file
64
+
65
+ ## Resource Management
66
+
67
+ ### Apply Resources
68
+
69
+ ```yaml
70
+ # bucket.yaml
71
+ provider: gcp
72
+ resource: storage
73
+ name: my-bucket
74
+ config:
75
+ location: US
76
+ storage_class: STANDARD
77
+ ```
78
+
79
+ ```bash
80
+ # Apply from file
81
+ pragma resources apply bucket.yaml
82
+
83
+ # Apply multiple files
84
+ pragma resources apply *.yaml
85
+
86
+ # Apply with pending flag to execute immediately
87
+ pragma resources apply --pending bucket.yaml
88
+ ```
89
+
90
+ ### List and Get Resources
91
+
92
+ ```bash
93
+ # List all resources
94
+ pragma resources list
95
+
96
+ # Filter by provider
97
+ pragma resources list --provider gcp
98
+
99
+ # Filter by resource type
100
+ pragma resources list --resource storage
101
+
102
+ # Get specific resource
103
+ pragma resources get gcp/storage my-bucket
104
+ ```
105
+
106
+ ### Delete Resources
107
+
108
+ ```bash
109
+ pragma resources delete gcp/storage my-bucket
110
+ ```
111
+
112
+ ## Provider Development
113
+
114
+ Build and deploy custom providers:
115
+
116
+ ```bash
117
+ # Initialize a new provider project
118
+ pragma provider init mycompany
119
+
120
+ # Sync resource schemas with the platform
121
+ pragma provider sync
122
+
123
+ # Build and deploy
124
+ pragma provider push --deploy
125
+ ```
126
+
127
+ ## Authentication
128
+
129
+ ```bash
130
+ # Login (opens browser)
131
+ pragma auth login
132
+
133
+ # Check current user
134
+ pragma auth whoami
135
+
136
+ # Logout
137
+ pragma auth logout
138
+ ```
139
+
140
+ ## Configuration
141
+
142
+ Set environment variables to configure the CLI:
143
+
144
+ ```bash
145
+ export PRAGMA_API_URL=https://api.pragmatiks.io
146
+ export PRAGMA_AUTH_TOKEN=sk_...
147
+ ```
148
+
149
+ ## Command Reference
150
+
151
+ ### Resources
152
+
153
+ | Command | Description |
154
+ |---------|-------------|
155
+ | `pragma resources list` | List resources with optional filters |
156
+ | `pragma resources get <provider/resource> <name>` | Get a specific resource |
157
+ | `pragma resources apply <file>` | Apply resources from YAML |
158
+ | `pragma resources delete <provider/resource> <name>` | Delete a resource |
159
+
160
+ ### Providers
161
+
162
+ | Command | Description |
163
+ |---------|-------------|
164
+ | `pragma provider init <name>` | Initialize a new provider project |
165
+ | `pragma provider sync` | Sync resource schemas with platform |
166
+ | `pragma provider push` | Build and push provider image |
167
+ | `pragma provider push --deploy` | Build, push, and deploy |
168
+
169
+ ### Authentication
170
+
171
+ | Command | Description |
172
+ |---------|-------------|
173
+ | `pragma auth login` | Authenticate with the platform |
174
+ | `pragma auth whoami` | Show current user |
175
+ | `pragma auth logout` | Clear credentials |
176
+
177
+ ### Operations
178
+
179
+ | Command | Description |
180
+ |---------|-------------|
181
+ | `pragma ops dead-letter list` | List failed events |
182
+ | `pragma ops dead-letter retry <id>` | Retry a failed event |
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ # Run tests
188
+ task cli:test
189
+
190
+ # Format code
191
+ task cli:format
192
+
193
+ # Type check and lint
194
+ task cli:check
195
+ ```
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,17 @@
1
+ pragma_cli/__init__.py,sha256=9REbOdKs9CeuOd-rxeFs17gWtou1dUdCogYU8G5Cz6c,682
2
+ pragma_cli/commands/__init__.py,sha256=zltFPaCZgkeTdOH1YWrUEqqBF9Dg6tokgAFcmqP4_n4,24
3
+ pragma_cli/commands/auth.py,sha256=6VqLTD8sZ7LRmI8RXEkSze1Ctnq5oM_1Zv0Br-HuFIA,8198
4
+ pragma_cli/commands/completions.py,sha256=ZCW38A1-6l_IcGC7Gj2CP5VL6PzaY4zKdcxylDuCoWM,1609
5
+ pragma_cli/commands/config.py,sha256=Bow71Tg_zeprHlbn_6y8g2YsV_k9nRobA6ooYfaxtWE,2281
6
+ pragma_cli/commands/dead_letter.py,sha256=8Mh_QVZiwkbOA1fYkw1O9BeHgPdqepis6tSJOAY3vhA,6754
7
+ pragma_cli/commands/ops.py,sha256=ztx0Gx2L2mEqJQpbgDHgfOUZ4uaD132NxgKohaPOWv8,361
8
+ pragma_cli/commands/provider.py,sha256=BX1NqGbH9XOrYFrWb92UpZhjTJA5qJ_U6cppMB1_lq0,36271
9
+ pragma_cli/commands/resources.py,sha256=BzwFKK5vqDo37wUPwRkulKdC4TFEJEoSXLlzsxQFD6w,6904
10
+ pragma_cli/config.py,sha256=kcU4tJUV1DfnXC_ydQbgQoJzjdGB1s-6-7g4cwA1nZw,2340
11
+ pragma_cli/helpers.py,sha256=dVxokT-sqF08oY0O35QZ64QyNC0PHZEBJYTvZ726UtI,650
12
+ pragma_cli/main.py,sha256=3UAKMsgc5u2v55ykWy-o_XC5W2CwyDNGfTSFhSvsLfU,1970
13
+ pragma_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ pragmatiks_cli-0.5.1.dist-info/WHEEL,sha256=XjEbIc5-wIORjWaafhI6vBtlxDBp7S9KiujWF1EM7Ak,79
15
+ pragmatiks_cli-0.5.1.dist-info/entry_points.txt,sha256=9xeQQlnHxq94dks6mlJ2I9LuMUKmqxuJzyKSZCb9iJM,48
16
+ pragmatiks_cli-0.5.1.dist-info/METADATA,sha256=N3AEoBl0p7bLIpxYtZtI7mltnY4xdKVHnO9iSIg4njs,4400
17
+ pragmatiks_cli-0.5.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.25
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pragma = pragma_cli.main:app
3
+