pmctl 0.1.0__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.
- pmctl-0.1.0/.claude/settings.local.json +8 -0
- pmctl-0.1.0/.gitignore +13 -0
- pmctl-0.1.0/PKG-INFO +138 -0
- pmctl-0.1.0/README.md +112 -0
- pmctl-0.1.0/pyproject.toml +47 -0
- pmctl-0.1.0/src/pmctl/__init__.py +3 -0
- pmctl-0.1.0/src/pmctl/api.py +74 -0
- pmctl-0.1.0/src/pmctl/cli.py +441 -0
- pmctl-0.1.0/src/pmctl/config.py +154 -0
- pmctl-0.1.0/uv.lock +194 -0
pmctl-0.1.0/.gitignore
ADDED
pmctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI tool for managing Postman collections, environments, and workspaces
|
|
5
|
+
Project-URL: Homepage, https://github.com/wbinglee/pmctl
|
|
6
|
+
Project-URL: Repository, https://github.com/wbinglee/pmctl
|
|
7
|
+
Project-URL: Issues, https://github.com/wbinglee/pmctl/issues
|
|
8
|
+
Author: Wenbing Li
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: api,cli,collections,postman
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Requires-Dist: typer>=0.9.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# pmctl
|
|
28
|
+
|
|
29
|
+
A CLI tool for managing Postman collections, environments, and workspaces.
|
|
30
|
+
|
|
31
|
+
The official Postman CLI only supports running collections. `pmctl` fills the gap by wrapping the [Postman API](https://www.postman.com/postman/postman-public-workspace/documentation/12946884/postman-api) to let you browse and manage your Postman resources from the terminal.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- 🔑 **Multi-profile support** — manage multiple Postman accounts (personal, work, etc.)
|
|
36
|
+
- 📦 **Browse collections** — list and inspect collections with a beautiful tree view
|
|
37
|
+
- 🌍 **Environments** — list environments and view variables
|
|
38
|
+
- 🏢 **Workspaces** — list all accessible workspaces
|
|
39
|
+
- 🎨 **Rich output** — colored tables and trees powered by [Rich](https://github.com/Textualize/rich)
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install pmctl
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or install from source:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/wbinglee/pmctl.git
|
|
51
|
+
cd pmctl
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
### 1. Add a profile
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Add your Postman API key (get one at https://go.postman.co/settings/me/api-keys)
|
|
61
|
+
pmctl profile add personal --api-key "PMAK-..." --label "Personal Account" --default
|
|
62
|
+
|
|
63
|
+
# Add another profile (e.g., work account)
|
|
64
|
+
pmctl profile add work --api-key "PMAK-..." --label "Work Account"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Browse your resources
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# List collections
|
|
71
|
+
pmctl collections list
|
|
72
|
+
|
|
73
|
+
# List collections in a specific workspace
|
|
74
|
+
pmctl collections list --workspace <workspace-id>
|
|
75
|
+
|
|
76
|
+
# Show all requests in a collection (tree view)
|
|
77
|
+
pmctl collections show <collection-uid>
|
|
78
|
+
|
|
79
|
+
# List environments
|
|
80
|
+
pmctl environments list
|
|
81
|
+
|
|
82
|
+
# Show environment variables
|
|
83
|
+
pmctl environments show <env-id> --values
|
|
84
|
+
|
|
85
|
+
# List workspaces
|
|
86
|
+
pmctl workspaces list
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Switch between profiles
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Switch default profile
|
|
93
|
+
pmctl profile switch work
|
|
94
|
+
|
|
95
|
+
# Or use --profile flag on any command
|
|
96
|
+
pmctl collections list --profile personal
|
|
97
|
+
|
|
98
|
+
# Check who you're logged in as
|
|
99
|
+
pmctl profile whoami
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Profile Management
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pmctl profile list # List all profiles
|
|
106
|
+
pmctl profile add <name> # Add a new profile
|
|
107
|
+
pmctl profile remove <name> # Remove a profile
|
|
108
|
+
pmctl profile switch <name> # Set default profile
|
|
109
|
+
pmctl profile whoami # Show current user info
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
Profiles are stored in `~/.config/pmctl/config.toml`:
|
|
115
|
+
|
|
116
|
+
```toml
|
|
117
|
+
[profiles.personal]
|
|
118
|
+
api_key = "PMAK-..."
|
|
119
|
+
label = "personal@example.com"
|
|
120
|
+
|
|
121
|
+
[profiles.work]
|
|
122
|
+
api_key = "PMAK-..."
|
|
123
|
+
label = "work@company.com"
|
|
124
|
+
|
|
125
|
+
default_profile = "work"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Getting a Postman API Key
|
|
129
|
+
|
|
130
|
+
1. Go to [Postman API Keys](https://go.postman.co/settings/me/api-keys)
|
|
131
|
+
2. Click **Generate API Key**
|
|
132
|
+
3. Copy the key and add it with `pmctl profile add`
|
|
133
|
+
|
|
134
|
+
> **Note:** If you have multiple Postman accounts (e.g., personal + company SSO), each account has its own API keys page. Log into the correct account first.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
pmctl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# pmctl
|
|
2
|
+
|
|
3
|
+
A CLI tool for managing Postman collections, environments, and workspaces.
|
|
4
|
+
|
|
5
|
+
The official Postman CLI only supports running collections. `pmctl` fills the gap by wrapping the [Postman API](https://www.postman.com/postman/postman-public-workspace/documentation/12946884/postman-api) to let you browse and manage your Postman resources from the terminal.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔑 **Multi-profile support** — manage multiple Postman accounts (personal, work, etc.)
|
|
10
|
+
- 📦 **Browse collections** — list and inspect collections with a beautiful tree view
|
|
11
|
+
- 🌍 **Environments** — list environments and view variables
|
|
12
|
+
- 🏢 **Workspaces** — list all accessible workspaces
|
|
13
|
+
- 🎨 **Rich output** — colored tables and trees powered by [Rich](https://github.com/Textualize/rich)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install pmctl
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install from source:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/wbinglee/pmctl.git
|
|
25
|
+
cd pmctl
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Add a profile
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Add your Postman API key (get one at https://go.postman.co/settings/me/api-keys)
|
|
35
|
+
pmctl profile add personal --api-key "PMAK-..." --label "Personal Account" --default
|
|
36
|
+
|
|
37
|
+
# Add another profile (e.g., work account)
|
|
38
|
+
pmctl profile add work --api-key "PMAK-..." --label "Work Account"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Browse your resources
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# List collections
|
|
45
|
+
pmctl collections list
|
|
46
|
+
|
|
47
|
+
# List collections in a specific workspace
|
|
48
|
+
pmctl collections list --workspace <workspace-id>
|
|
49
|
+
|
|
50
|
+
# Show all requests in a collection (tree view)
|
|
51
|
+
pmctl collections show <collection-uid>
|
|
52
|
+
|
|
53
|
+
# List environments
|
|
54
|
+
pmctl environments list
|
|
55
|
+
|
|
56
|
+
# Show environment variables
|
|
57
|
+
pmctl environments show <env-id> --values
|
|
58
|
+
|
|
59
|
+
# List workspaces
|
|
60
|
+
pmctl workspaces list
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Switch between profiles
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Switch default profile
|
|
67
|
+
pmctl profile switch work
|
|
68
|
+
|
|
69
|
+
# Or use --profile flag on any command
|
|
70
|
+
pmctl collections list --profile personal
|
|
71
|
+
|
|
72
|
+
# Check who you're logged in as
|
|
73
|
+
pmctl profile whoami
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Profile Management
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pmctl profile list # List all profiles
|
|
80
|
+
pmctl profile add <name> # Add a new profile
|
|
81
|
+
pmctl profile remove <name> # Remove a profile
|
|
82
|
+
pmctl profile switch <name> # Set default profile
|
|
83
|
+
pmctl profile whoami # Show current user info
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
Profiles are stored in `~/.config/pmctl/config.toml`:
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[profiles.personal]
|
|
92
|
+
api_key = "PMAK-..."
|
|
93
|
+
label = "personal@example.com"
|
|
94
|
+
|
|
95
|
+
[profiles.work]
|
|
96
|
+
api_key = "PMAK-..."
|
|
97
|
+
label = "work@company.com"
|
|
98
|
+
|
|
99
|
+
default_profile = "work"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Getting a Postman API Key
|
|
103
|
+
|
|
104
|
+
1. Go to [Postman API Keys](https://go.postman.co/settings/me/api-keys)
|
|
105
|
+
2. Click **Generate API Key**
|
|
106
|
+
3. Copy the key and add it with `pmctl profile add`
|
|
107
|
+
|
|
108
|
+
> **Note:** If you have multiple Postman accounts (e.g., personal + company SSO), each account has its own API keys page. Log into the correct account first.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pmctl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A CLI tool for managing Postman collections, environments, and workspaces"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Wenbing Li" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["postman", "cli", "api", "collections"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
"Topic :: Utilities",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"typer>=0.9.0",
|
|
30
|
+
"httpx>=0.25.0",
|
|
31
|
+
"rich>=13.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
pmctl = "pmctl.cli:app"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/wbinglee/pmctl"
|
|
39
|
+
Repository = "https://github.com/wbinglee/pmctl"
|
|
40
|
+
Issues = "https://github.com/wbinglee/pmctl/issues"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/pmctl"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
target-version = "py311"
|
|
47
|
+
line-length = 100
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Postman API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
BASE_URL = "https://api.getpostman.com"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PostmanClient:
|
|
13
|
+
"""HTTP client for the Postman API."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, api_key: str):
|
|
16
|
+
self.api_key = api_key
|
|
17
|
+
self._client = httpx.Client(
|
|
18
|
+
base_url=BASE_URL,
|
|
19
|
+
headers={"X-Api-Key": api_key},
|
|
20
|
+
timeout=30.0,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def _get(self, path: str, params: Optional[dict] = None) -> dict[str, Any]:
|
|
24
|
+
"""Make a GET request to the Postman API."""
|
|
25
|
+
response = self._client.get(path, params=params)
|
|
26
|
+
response.raise_for_status()
|
|
27
|
+
return response.json()
|
|
28
|
+
|
|
29
|
+
def close(self) -> None:
|
|
30
|
+
self._client.close()
|
|
31
|
+
|
|
32
|
+
def __enter__(self) -> "PostmanClient":
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __exit__(self, *args: Any) -> None:
|
|
36
|
+
self.close()
|
|
37
|
+
|
|
38
|
+
# --- User ---
|
|
39
|
+
|
|
40
|
+
def get_me(self) -> dict[str, Any]:
|
|
41
|
+
"""Get current user info."""
|
|
42
|
+
return self._get("/me")
|
|
43
|
+
|
|
44
|
+
# --- Workspaces ---
|
|
45
|
+
|
|
46
|
+
def list_workspaces(self) -> list[dict[str, Any]]:
|
|
47
|
+
"""List all accessible workspaces."""
|
|
48
|
+
return self._get("/workspaces").get("workspaces", [])
|
|
49
|
+
|
|
50
|
+
def get_workspace(self, workspace_id: str) -> dict[str, Any]:
|
|
51
|
+
"""Get workspace details."""
|
|
52
|
+
return self._get(f"/workspaces/{workspace_id}").get("workspace", {})
|
|
53
|
+
|
|
54
|
+
# --- Collections ---
|
|
55
|
+
|
|
56
|
+
def list_collections(self, workspace_id: Optional[str] = None) -> list[dict[str, Any]]:
|
|
57
|
+
"""List collections, optionally filtered by workspace."""
|
|
58
|
+
params = {"workspace": workspace_id} if workspace_id else None
|
|
59
|
+
return self._get("/collections", params=params).get("collections", [])
|
|
60
|
+
|
|
61
|
+
def get_collection(self, collection_uid: str) -> dict[str, Any]:
|
|
62
|
+
"""Get full collection details including all requests."""
|
|
63
|
+
return self._get(f"/collections/{collection_uid}").get("collection", {})
|
|
64
|
+
|
|
65
|
+
# --- Environments ---
|
|
66
|
+
|
|
67
|
+
def list_environments(self, workspace_id: Optional[str] = None) -> list[dict[str, Any]]:
|
|
68
|
+
"""List environments, optionally filtered by workspace."""
|
|
69
|
+
params = {"workspace": workspace_id} if workspace_id else None
|
|
70
|
+
return self._get("/environments", params=params).get("environments", [])
|
|
71
|
+
|
|
72
|
+
def get_environment(self, environment_id: str) -> dict[str, Any]:
|
|
73
|
+
"""Get environment details including variables."""
|
|
74
|
+
return self._get(f"/environments/{environment_id}").get("environment", {})
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""pmctl - CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.tree import Tree
|
|
11
|
+
|
|
12
|
+
from pmctl.api import PostmanClient
|
|
13
|
+
from pmctl.config import (
|
|
14
|
+
add_profile,
|
|
15
|
+
load_config,
|
|
16
|
+
remove_profile,
|
|
17
|
+
set_default_profile,
|
|
18
|
+
set_profile_workspace,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(
|
|
22
|
+
name="pmctl",
|
|
23
|
+
help="A CLI tool for managing Postman collections, environments, and workspaces.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
# --- Profile subcommand ---
|
|
29
|
+
|
|
30
|
+
profile_app = typer.Typer(help="Manage Postman API key profiles.", no_args_is_help=True)
|
|
31
|
+
app.add_typer(profile_app, name="profile")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@profile_app.command("list")
|
|
35
|
+
def profile_list():
|
|
36
|
+
"""List all configured profiles."""
|
|
37
|
+
config = load_config()
|
|
38
|
+
table = Table(title="Profiles")
|
|
39
|
+
table.add_column("Name", style="cyan")
|
|
40
|
+
table.add_column("Label", style="dim")
|
|
41
|
+
table.add_column("Default", style="green")
|
|
42
|
+
table.add_column("Workspace", style="dim")
|
|
43
|
+
table.add_column("API Key", style="dim")
|
|
44
|
+
|
|
45
|
+
for prof_name, profile in config.profiles.items():
|
|
46
|
+
is_default = "✓" if prof_name == config.default_profile else ""
|
|
47
|
+
masked_key = profile.api_key[:12] + "..." + profile.api_key[-4:]
|
|
48
|
+
ws = profile.workspace[:12] + "..." if len(profile.workspace) > 12 else profile.workspace
|
|
49
|
+
table.add_row(prof_name, profile.label, is_default, ws, masked_key)
|
|
50
|
+
|
|
51
|
+
console.print(table)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@profile_app.command("add")
|
|
55
|
+
def profile_add(
|
|
56
|
+
name: str = typer.Argument(help="Profile name"),
|
|
57
|
+
api_key: str = typer.Option(..., "--api-key", "-k", help="Postman API key"),
|
|
58
|
+
label: str = typer.Option("", "--label", "-l", help="Description label"),
|
|
59
|
+
default: bool = typer.Option(False, "--default", "-d", help="Set as default profile"),
|
|
60
|
+
):
|
|
61
|
+
"""Add a new profile."""
|
|
62
|
+
config = add_profile(name, api_key, label, set_default=default)
|
|
63
|
+
console.print(f"[green]✓[/] Profile '{name}' added.")
|
|
64
|
+
if name == config.default_profile:
|
|
65
|
+
console.print(f"[green]✓[/] Set as default profile.")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@profile_app.command("remove")
|
|
69
|
+
def profile_remove(name: str = typer.Argument(help="Profile name to remove")):
|
|
70
|
+
"""Remove a profile."""
|
|
71
|
+
remove_profile(name)
|
|
72
|
+
console.print(f"[green]✓[/] Profile '{name}' removed.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@profile_app.command("switch")
|
|
76
|
+
def profile_switch(name: str = typer.Argument(help="Profile name to set as default")):
|
|
77
|
+
"""Switch the default profile."""
|
|
78
|
+
set_default_profile(name)
|
|
79
|
+
console.print(f"[green]✓[/] Default profile switched to '{name}'.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@profile_app.command("set-workspace")
|
|
83
|
+
def profile_set_workspace(
|
|
84
|
+
workspace_id: str = typer.Argument(help="Default workspace ID for this profile"),
|
|
85
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to update (default: current default)"),
|
|
86
|
+
):
|
|
87
|
+
"""Set the default workspace for a profile."""
|
|
88
|
+
config = set_profile_workspace(profile or "", workspace_id)
|
|
89
|
+
name = profile or config.default_profile
|
|
90
|
+
console.print(f"[green]✓[/] Default workspace for '{name}' set to '{workspace_id}'.")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@profile_app.command("whoami")
|
|
94
|
+
def profile_whoami(
|
|
95
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
96
|
+
):
|
|
97
|
+
"""Show current user info for the active profile."""
|
|
98
|
+
config = load_config()
|
|
99
|
+
p = config.get_profile(profile)
|
|
100
|
+
with PostmanClient(p.api_key) as client:
|
|
101
|
+
user = client.get_me().get("user", {})
|
|
102
|
+
console.print(f"[cyan]Email:[/] {user.get('email', 'N/A')}")
|
|
103
|
+
console.print(f"[cyan]Name:[/] {user.get('fullName', 'N/A')}")
|
|
104
|
+
console.print(f"[cyan]Team:[/] {user.get('teamName', 'N/A')}")
|
|
105
|
+
console.print(f"[cyan]Domain:[/] {user.get('teamDomain', 'N/A')}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# --- Workspaces subcommand ---
|
|
109
|
+
|
|
110
|
+
workspace_app = typer.Typer(help="Manage Postman workspaces.", no_args_is_help=True)
|
|
111
|
+
app.add_typer(workspace_app, name="workspaces")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@workspace_app.command("list")
|
|
115
|
+
def workspaces_list(
|
|
116
|
+
search: Optional[str] = typer.Option(None, "--search", "-s", help="Filter workspaces by name (case-insensitive)"),
|
|
117
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
118
|
+
):
|
|
119
|
+
"""List all accessible workspaces."""
|
|
120
|
+
config = load_config()
|
|
121
|
+
p = config.get_profile(profile)
|
|
122
|
+
with PostmanClient(p.api_key) as client:
|
|
123
|
+
workspaces = client.list_workspaces()
|
|
124
|
+
|
|
125
|
+
if search:
|
|
126
|
+
keyword = search.lower()
|
|
127
|
+
workspaces = [ws for ws in workspaces if keyword in ws["name"].lower()]
|
|
128
|
+
|
|
129
|
+
table = Table(title=f"Workspaces ({p.label or p.name})")
|
|
130
|
+
table.add_column("Name", style="cyan")
|
|
131
|
+
table.add_column("ID", style="dim")
|
|
132
|
+
table.add_column("Type", style="green")
|
|
133
|
+
|
|
134
|
+
for ws in sorted(workspaces, key=lambda w: w["name"].lower()):
|
|
135
|
+
table.add_row(ws["name"], ws["id"], ws.get("type", ""))
|
|
136
|
+
|
|
137
|
+
console.print(table)
|
|
138
|
+
console.print(f"\n[dim]Total: {len(workspaces)} workspaces[/]")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# --- Collections subcommand ---
|
|
142
|
+
|
|
143
|
+
collection_app = typer.Typer(help="Manage Postman collections.", no_args_is_help=True)
|
|
144
|
+
app.add_typer(collection_app, name="collections")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@collection_app.command("list")
|
|
148
|
+
def collections_list(
|
|
149
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Filter by workspace ID"),
|
|
150
|
+
all_workspaces: bool = typer.Option(False, "--all", "-a", help="Show collections from all workspaces"),
|
|
151
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
152
|
+
):
|
|
153
|
+
"""List collections."""
|
|
154
|
+
config = load_config()
|
|
155
|
+
p = config.get_profile(profile)
|
|
156
|
+
effective_workspace = workspace or (None if all_workspaces else p.workspace or None)
|
|
157
|
+
with PostmanClient(p.api_key) as client:
|
|
158
|
+
collections = client.list_collections(workspace_id=effective_workspace)
|
|
159
|
+
|
|
160
|
+
table = Table(title=f"Collections ({p.label or p.name})")
|
|
161
|
+
table.add_column("Name", style="cyan")
|
|
162
|
+
table.add_column("UID", style="dim")
|
|
163
|
+
table.add_column("Updated", style="dim")
|
|
164
|
+
|
|
165
|
+
for col in sorted(collections, key=lambda c: c["name"].lower()):
|
|
166
|
+
updated = col.get("updatedAt", "")[:10]
|
|
167
|
+
table.add_row(col["name"], col["uid"], updated)
|
|
168
|
+
|
|
169
|
+
console.print(table)
|
|
170
|
+
console.print(f"\n[dim]Total: {len(collections)} collections[/]")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@collection_app.command("show")
|
|
174
|
+
def collections_show(
|
|
175
|
+
uid: str = typer.Argument(help="Collection UID"),
|
|
176
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
177
|
+
):
|
|
178
|
+
"""Show all requests in a collection as a tree."""
|
|
179
|
+
config = load_config()
|
|
180
|
+
p = config.get_profile(profile)
|
|
181
|
+
with PostmanClient(p.api_key) as client:
|
|
182
|
+
collection = client.get_collection(uid)
|
|
183
|
+
|
|
184
|
+
tree = Tree(f"[bold cyan]{collection.get('info', {}).get('name', 'Collection')}[/]")
|
|
185
|
+
_build_tree(tree, collection.get("item", []))
|
|
186
|
+
console.print(tree)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _build_tree(tree: Tree, items: list) -> None:
|
|
190
|
+
"""Recursively build a Rich tree from collection items."""
|
|
191
|
+
for item in items:
|
|
192
|
+
if "item" in item:
|
|
193
|
+
# It's a folder
|
|
194
|
+
branch = tree.add(f"📁 [bold]{item['name']}[/]")
|
|
195
|
+
_build_tree(branch, item["item"])
|
|
196
|
+
else:
|
|
197
|
+
# It's a request
|
|
198
|
+
req = item.get("request", {})
|
|
199
|
+
method = req.get("method", "?")
|
|
200
|
+
url = req.get("url", {})
|
|
201
|
+
raw_url = url.get("raw", url) if isinstance(url, dict) else url
|
|
202
|
+
|
|
203
|
+
method_colors = {
|
|
204
|
+
"GET": "green",
|
|
205
|
+
"POST": "yellow",
|
|
206
|
+
"PUT": "blue",
|
|
207
|
+
"PATCH": "magenta",
|
|
208
|
+
"DELETE": "red",
|
|
209
|
+
}
|
|
210
|
+
color = method_colors.get(method, "white")
|
|
211
|
+
tree.add(f"[bold {color}]{method:7s}[/] {item['name']} [dim]{raw_url}[/]")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@collection_app.command("request")
|
|
215
|
+
def collections_request(
|
|
216
|
+
uid: str = typer.Argument(help="Collection UID"),
|
|
217
|
+
name: str = typer.Argument(help="Request name (case-insensitive substring match)"),
|
|
218
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
219
|
+
):
|
|
220
|
+
"""Show details of a specific request in a collection."""
|
|
221
|
+
config = load_config()
|
|
222
|
+
p = config.get_profile(profile)
|
|
223
|
+
with PostmanClient(p.api_key) as client:
|
|
224
|
+
collection = client.get_collection(uid)
|
|
225
|
+
|
|
226
|
+
matches = _find_requests(collection.get("item", []), name)
|
|
227
|
+
if not matches:
|
|
228
|
+
console.print(f"[red]No request matching '{name}' found.[/]")
|
|
229
|
+
raise typer.Exit(1)
|
|
230
|
+
if len(matches) > 1:
|
|
231
|
+
console.print(f"[yellow]Multiple matches found ({len(matches)}). Showing first match.[/]")
|
|
232
|
+
for i, (path, _) in enumerate(matches):
|
|
233
|
+
console.print(f" [dim]{i + 1}. {path}[/]")
|
|
234
|
+
console.print()
|
|
235
|
+
|
|
236
|
+
path, item = matches[0]
|
|
237
|
+
req = item.get("request", {})
|
|
238
|
+
method = req.get("method", "?")
|
|
239
|
+
url = req.get("url", {})
|
|
240
|
+
raw_url = url.get("raw", url) if isinstance(url, dict) else url
|
|
241
|
+
|
|
242
|
+
method_colors = {
|
|
243
|
+
"GET": "green", "POST": "yellow", "PUT": "blue",
|
|
244
|
+
"PATCH": "magenta", "DELETE": "red",
|
|
245
|
+
}
|
|
246
|
+
color = method_colors.get(method, "white")
|
|
247
|
+
|
|
248
|
+
console.print(f"[bold]{path}[/]\n")
|
|
249
|
+
console.print(f"[bold {color}]{method}[/] {raw_url}\n")
|
|
250
|
+
|
|
251
|
+
# Auth
|
|
252
|
+
auth = req.get("auth")
|
|
253
|
+
if auth:
|
|
254
|
+
auth_type = auth.get("type", "unknown")
|
|
255
|
+
console.print(f"[cyan]Auth:[/] {auth_type}")
|
|
256
|
+
|
|
257
|
+
# Headers
|
|
258
|
+
headers = req.get("header", [])
|
|
259
|
+
if headers:
|
|
260
|
+
table = Table(title="Headers", show_edge=False)
|
|
261
|
+
table.add_column("Key", style="cyan")
|
|
262
|
+
table.add_column("Value")
|
|
263
|
+
table.add_column("Enabled", style="dim")
|
|
264
|
+
for h in headers:
|
|
265
|
+
enabled = "✗" if h.get("disabled") else "✓"
|
|
266
|
+
table.add_row(h.get("key", ""), h.get("value", ""), enabled)
|
|
267
|
+
console.print(table)
|
|
268
|
+
|
|
269
|
+
# Query params
|
|
270
|
+
if isinstance(url, dict):
|
|
271
|
+
query = url.get("query", [])
|
|
272
|
+
if query:
|
|
273
|
+
table = Table(title="Query Params", show_edge=False)
|
|
274
|
+
table.add_column("Key", style="cyan")
|
|
275
|
+
table.add_column("Value")
|
|
276
|
+
table.add_column("Enabled", style="dim")
|
|
277
|
+
for q in query:
|
|
278
|
+
enabled = "✗" if q.get("disabled") else "✓"
|
|
279
|
+
table.add_row(q.get("key", ""), q.get("value", ""), enabled)
|
|
280
|
+
console.print(table)
|
|
281
|
+
|
|
282
|
+
# Path variables
|
|
283
|
+
variables = url.get("variable", [])
|
|
284
|
+
if variables:
|
|
285
|
+
table = Table(title="Path Variables", show_edge=False)
|
|
286
|
+
table.add_column("Key", style="cyan")
|
|
287
|
+
table.add_column("Value")
|
|
288
|
+
for v in variables:
|
|
289
|
+
table.add_row(v.get("key", ""), v.get("value", ""))
|
|
290
|
+
console.print(table)
|
|
291
|
+
|
|
292
|
+
# Body
|
|
293
|
+
body = req.get("body")
|
|
294
|
+
if body:
|
|
295
|
+
mode = body.get("mode", "")
|
|
296
|
+
console.print(f"\n[cyan]Body[/] [dim]({mode})[/]")
|
|
297
|
+
if mode == "raw":
|
|
298
|
+
raw = body.get("raw", "")
|
|
299
|
+
if raw:
|
|
300
|
+
console.print(raw)
|
|
301
|
+
elif mode == "formdata":
|
|
302
|
+
table = Table(show_edge=False)
|
|
303
|
+
table.add_column("Key", style="cyan")
|
|
304
|
+
table.add_column("Value")
|
|
305
|
+
table.add_column("Type", style="dim")
|
|
306
|
+
for fd in body.get("formdata", []):
|
|
307
|
+
table.add_row(fd.get("key", ""), fd.get("value", ""), fd.get("type", "text"))
|
|
308
|
+
console.print(table)
|
|
309
|
+
elif mode == "urlencoded":
|
|
310
|
+
table = Table(show_edge=False)
|
|
311
|
+
table.add_column("Key", style="cyan")
|
|
312
|
+
table.add_column("Value")
|
|
313
|
+
for ue in body.get("urlencoded", []):
|
|
314
|
+
table.add_row(ue.get("key", ""), ue.get("value", ""))
|
|
315
|
+
console.print(table)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _find_requests(items: list, name: str, prefix: str = "") -> list[tuple[str, dict]]:
|
|
319
|
+
"""Recursively find requests matching a name (case-insensitive substring)."""
|
|
320
|
+
matches = []
|
|
321
|
+
keyword = name.lower()
|
|
322
|
+
for item in items:
|
|
323
|
+
path = f"{prefix}/{item['name']}" if prefix else item["name"]
|
|
324
|
+
if "item" in item:
|
|
325
|
+
matches.extend(_find_requests(item["item"], name, path))
|
|
326
|
+
elif keyword in item["name"].lower():
|
|
327
|
+
matches.append((path, item))
|
|
328
|
+
return matches
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# --- Environments subcommand ---
|
|
332
|
+
|
|
333
|
+
env_app = typer.Typer(help="Manage Postman environments.", no_args_is_help=True)
|
|
334
|
+
app.add_typer(env_app, name="environments")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@env_app.command("list")
|
|
338
|
+
def environments_list(
|
|
339
|
+
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Filter by workspace ID"),
|
|
340
|
+
all_workspaces: bool = typer.Option(False, "--all", "-a", help="Show environments from all workspaces"),
|
|
341
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
342
|
+
):
|
|
343
|
+
"""List environments."""
|
|
344
|
+
config = load_config()
|
|
345
|
+
p = config.get_profile(profile)
|
|
346
|
+
effective_workspace = workspace or (None if all_workspaces else p.workspace or None)
|
|
347
|
+
with PostmanClient(p.api_key) as client:
|
|
348
|
+
environments = client.list_environments(workspace_id=effective_workspace)
|
|
349
|
+
|
|
350
|
+
table = Table(title=f"Environments ({p.label or p.name})")
|
|
351
|
+
table.add_column("Name", style="cyan")
|
|
352
|
+
table.add_column("ID", style="dim")
|
|
353
|
+
|
|
354
|
+
for env in sorted(environments, key=lambda e: e["name"].lower()):
|
|
355
|
+
table.add_row(env["name"], env["id"])
|
|
356
|
+
|
|
357
|
+
console.print(table)
|
|
358
|
+
console.print(f"\n[dim]Total: {len(environments)} environments[/]")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@env_app.command("show")
|
|
362
|
+
def environments_show(
|
|
363
|
+
env_id: str = typer.Argument(help="Environment ID"),
|
|
364
|
+
show_values: bool = typer.Option(False, "--values", "-v", help="Show variable values"),
|
|
365
|
+
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
366
|
+
):
|
|
367
|
+
"""Show environment variables."""
|
|
368
|
+
config = load_config()
|
|
369
|
+
p = config.get_profile(profile)
|
|
370
|
+
with PostmanClient(p.api_key) as client:
|
|
371
|
+
env = client.get_environment(env_id)
|
|
372
|
+
|
|
373
|
+
console.print(f"[bold cyan]{env.get('name', 'Environment')}[/]\n")
|
|
374
|
+
|
|
375
|
+
table = Table()
|
|
376
|
+
table.add_column("Variable", style="cyan")
|
|
377
|
+
table.add_column("Type", style="dim")
|
|
378
|
+
if show_values:
|
|
379
|
+
table.add_column("Value")
|
|
380
|
+
|
|
381
|
+
for var in env.get("values", []):
|
|
382
|
+
row = [var["key"], var.get("type", "default")]
|
|
383
|
+
if show_values:
|
|
384
|
+
value = var.get("value", "")
|
|
385
|
+
# Mask sensitive-looking values
|
|
386
|
+
if any(k in var["key"].lower() for k in ("password", "secret", "token", "key")):
|
|
387
|
+
value = value[:4] + "****" if len(value) > 4 else "****"
|
|
388
|
+
row.append(value)
|
|
389
|
+
table.add_row(*row)
|
|
390
|
+
|
|
391
|
+
console.print(table)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# --- Completion subcommand ---
|
|
395
|
+
|
|
396
|
+
completion_app = typer.Typer(help="Generate shell completion scripts.", no_args_is_help=True)
|
|
397
|
+
app.add_typer(completion_app, name="completion")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _print_completion_script(shell: str) -> None:
|
|
401
|
+
"""Generate and print a shell completion script."""
|
|
402
|
+
from click.shell_completion import get_completion_class
|
|
403
|
+
|
|
404
|
+
click_app = typer.main.get_command(app)
|
|
405
|
+
comp_cls = get_completion_class(shell)
|
|
406
|
+
if comp_cls is None:
|
|
407
|
+
console.print(f"[red]Error:[/] Unsupported shell '{shell}'.")
|
|
408
|
+
raise typer.Exit(1)
|
|
409
|
+
comp = comp_cls(click_app, {}, "pmctl", "_PMCTL_COMPLETE")
|
|
410
|
+
typer.echo(comp.source())
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@completion_app.command("bash")
|
|
414
|
+
def completion_bash():
|
|
415
|
+
"""Generate bash completion script.
|
|
416
|
+
|
|
417
|
+
Usage: eval "$(pmctl completion bash)"
|
|
418
|
+
"""
|
|
419
|
+
_print_completion_script("bash")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@completion_app.command("zsh")
|
|
423
|
+
def completion_zsh():
|
|
424
|
+
"""Generate zsh completion script.
|
|
425
|
+
|
|
426
|
+
Usage: eval "$(pmctl completion zsh)"
|
|
427
|
+
"""
|
|
428
|
+
_print_completion_script("zsh")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@completion_app.command("fish")
|
|
432
|
+
def completion_fish():
|
|
433
|
+
"""Generate fish completion script.
|
|
434
|
+
|
|
435
|
+
Usage: pmctl completion fish > ~/.config/fish/completions/pmctl.fish
|
|
436
|
+
"""
|
|
437
|
+
_print_completion_script("fish")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
app()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Configuration management for pmctl.
|
|
2
|
+
|
|
3
|
+
Manages multiple Postman API key profiles stored in ~/.config/pmctl/config.toml.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import tomllib
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
CONFIG_DIR = Path.home() / ".config" / "pmctl"
|
|
14
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Profile:
|
|
19
|
+
"""A Postman API key profile."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
api_key: str
|
|
23
|
+
label: str = ""
|
|
24
|
+
workspace: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Config:
|
|
29
|
+
"""Application configuration."""
|
|
30
|
+
|
|
31
|
+
profiles: dict[str, Profile]
|
|
32
|
+
default_profile: str
|
|
33
|
+
|
|
34
|
+
def get_profile(self, name: Optional[str] = None) -> Profile:
|
|
35
|
+
"""Get a profile by name, or the default profile."""
|
|
36
|
+
profile_name = name or self.default_profile
|
|
37
|
+
if profile_name not in self.profiles:
|
|
38
|
+
available = ", ".join(self.profiles.keys())
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Profile '{profile_name}' not found. Available profiles: {available}"
|
|
41
|
+
)
|
|
42
|
+
return self.profiles[profile_name]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_config_dir() -> None:
|
|
46
|
+
"""Create config directory if it doesn't exist."""
|
|
47
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_config() -> Config:
|
|
51
|
+
"""Load configuration from TOML file."""
|
|
52
|
+
if not CONFIG_FILE.exists():
|
|
53
|
+
raise FileNotFoundError(
|
|
54
|
+
f"Config file not found at {CONFIG_FILE}\n"
|
|
55
|
+
f"Run 'pmctl profile add <name> --api-key <key>' to create one."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
59
|
+
data = tomllib.load(f)
|
|
60
|
+
|
|
61
|
+
profiles = {}
|
|
62
|
+
for name, profile_data in data.get("profiles", {}).items():
|
|
63
|
+
profiles[name] = Profile(
|
|
64
|
+
name=name,
|
|
65
|
+
api_key=profile_data["api_key"],
|
|
66
|
+
label=profile_data.get("label", ""),
|
|
67
|
+
workspace=profile_data.get("workspace", ""),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not profiles:
|
|
71
|
+
raise ValueError("No profiles found in config file.")
|
|
72
|
+
|
|
73
|
+
default = data.get("default_profile", next(iter(profiles)))
|
|
74
|
+
return Config(profiles=profiles, default_profile=default)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def save_config(config: Config) -> None:
|
|
78
|
+
"""Save configuration to TOML file."""
|
|
79
|
+
_ensure_config_dir()
|
|
80
|
+
|
|
81
|
+
lines = []
|
|
82
|
+
# Write top-level keys first (before any table sections)
|
|
83
|
+
lines.append(f'default_profile = "{config.default_profile}"')
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
for name, profile in config.profiles.items():
|
|
87
|
+
lines.append(f"[profiles.{name}]")
|
|
88
|
+
lines.append(f'api_key = "{profile.api_key}"')
|
|
89
|
+
if profile.label:
|
|
90
|
+
lines.append(f'label = "{profile.label}"')
|
|
91
|
+
if profile.workspace:
|
|
92
|
+
lines.append(f'workspace = "{profile.workspace}"')
|
|
93
|
+
lines.append("")
|
|
94
|
+
|
|
95
|
+
CONFIG_FILE.write_text("\n".join(lines))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def add_profile(name: str, api_key: str, label: str = "", workspace: str = "", set_default: bool = False) -> Config:
|
|
99
|
+
"""Add a new profile to the config."""
|
|
100
|
+
try:
|
|
101
|
+
config = load_config()
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
config = Config(profiles={}, default_profile=name)
|
|
104
|
+
|
|
105
|
+
config.profiles[name] = Profile(name=name, api_key=api_key, label=label, workspace=workspace)
|
|
106
|
+
|
|
107
|
+
if set_default or len(config.profiles) == 1:
|
|
108
|
+
config.default_profile = name
|
|
109
|
+
|
|
110
|
+
save_config(config)
|
|
111
|
+
return config
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def remove_profile(name: str) -> Config:
|
|
115
|
+
"""Remove a profile from the config."""
|
|
116
|
+
config = load_config()
|
|
117
|
+
|
|
118
|
+
if name not in config.profiles:
|
|
119
|
+
raise ValueError(f"Profile '{name}' not found.")
|
|
120
|
+
|
|
121
|
+
del config.profiles[name]
|
|
122
|
+
|
|
123
|
+
if config.default_profile == name:
|
|
124
|
+
config.default_profile = next(iter(config.profiles), "")
|
|
125
|
+
|
|
126
|
+
save_config(config)
|
|
127
|
+
return config
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def set_default_profile(name: str) -> Config:
|
|
131
|
+
"""Set the default profile."""
|
|
132
|
+
config = load_config()
|
|
133
|
+
|
|
134
|
+
if name not in config.profiles:
|
|
135
|
+
available = ", ".join(config.profiles.keys())
|
|
136
|
+
raise ValueError(f"Profile '{name}' not found. Available: {available}")
|
|
137
|
+
|
|
138
|
+
config.default_profile = name
|
|
139
|
+
save_config(config)
|
|
140
|
+
return config
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def set_profile_workspace(profile_name: str, workspace_id: str) -> Config:
|
|
144
|
+
"""Set the default workspace for a profile."""
|
|
145
|
+
config = load_config()
|
|
146
|
+
|
|
147
|
+
name = profile_name or config.default_profile
|
|
148
|
+
if name not in config.profiles:
|
|
149
|
+
available = ", ".join(config.profiles.keys())
|
|
150
|
+
raise ValueError(f"Profile '{name}' not found. Available: {available}")
|
|
151
|
+
|
|
152
|
+
config.profiles[name].workspace = workspace_id
|
|
153
|
+
save_config(config)
|
|
154
|
+
return config
|
pmctl-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "annotated-doc"
|
|
7
|
+
version = "0.0.4"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "anyio"
|
|
16
|
+
version = "4.12.1"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
dependencies = [
|
|
19
|
+
{ name = "idna" },
|
|
20
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
21
|
+
]
|
|
22
|
+
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
|
23
|
+
wheels = [
|
|
24
|
+
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[package]]
|
|
28
|
+
name = "certifi"
|
|
29
|
+
version = "2026.1.4"
|
|
30
|
+
source = { registry = "https://pypi.org/simple" }
|
|
31
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
|
32
|
+
wheels = [
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[[package]]
|
|
37
|
+
name = "click"
|
|
38
|
+
version = "8.3.1"
|
|
39
|
+
source = { registry = "https://pypi.org/simple" }
|
|
40
|
+
dependencies = [
|
|
41
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
42
|
+
]
|
|
43
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
|
44
|
+
wheels = [
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[[package]]
|
|
49
|
+
name = "colorama"
|
|
50
|
+
version = "0.4.6"
|
|
51
|
+
source = { registry = "https://pypi.org/simple" }
|
|
52
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
53
|
+
wheels = [
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[[package]]
|
|
58
|
+
name = "h11"
|
|
59
|
+
version = "0.16.0"
|
|
60
|
+
source = { registry = "https://pypi.org/simple" }
|
|
61
|
+
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
62
|
+
wheels = [
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[[package]]
|
|
67
|
+
name = "httpcore"
|
|
68
|
+
version = "1.0.9"
|
|
69
|
+
source = { registry = "https://pypi.org/simple" }
|
|
70
|
+
dependencies = [
|
|
71
|
+
{ name = "certifi" },
|
|
72
|
+
{ name = "h11" },
|
|
73
|
+
]
|
|
74
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
75
|
+
wheels = [
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[[package]]
|
|
80
|
+
name = "httpx"
|
|
81
|
+
version = "0.28.1"
|
|
82
|
+
source = { registry = "https://pypi.org/simple" }
|
|
83
|
+
dependencies = [
|
|
84
|
+
{ name = "anyio" },
|
|
85
|
+
{ name = "certifi" },
|
|
86
|
+
{ name = "httpcore" },
|
|
87
|
+
{ name = "idna" },
|
|
88
|
+
]
|
|
89
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
90
|
+
wheels = [
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[[package]]
|
|
95
|
+
name = "idna"
|
|
96
|
+
version = "3.11"
|
|
97
|
+
source = { registry = "https://pypi.org/simple" }
|
|
98
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
99
|
+
wheels = [
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[[package]]
|
|
104
|
+
name = "markdown-it-py"
|
|
105
|
+
version = "4.0.0"
|
|
106
|
+
source = { registry = "https://pypi.org/simple" }
|
|
107
|
+
dependencies = [
|
|
108
|
+
{ name = "mdurl" },
|
|
109
|
+
]
|
|
110
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
|
111
|
+
wheels = [
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
[[package]]
|
|
116
|
+
name = "mdurl"
|
|
117
|
+
version = "0.1.2"
|
|
118
|
+
source = { registry = "https://pypi.org/simple" }
|
|
119
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
120
|
+
wheels = [
|
|
121
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
[[package]]
|
|
125
|
+
name = "pmctl"
|
|
126
|
+
version = "0.1.0"
|
|
127
|
+
source = { editable = "." }
|
|
128
|
+
dependencies = [
|
|
129
|
+
{ name = "httpx" },
|
|
130
|
+
{ name = "rich" },
|
|
131
|
+
{ name = "typer" },
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
[package.metadata]
|
|
135
|
+
requires-dist = [
|
|
136
|
+
{ name = "httpx", specifier = ">=0.25.0" },
|
|
137
|
+
{ name = "rich", specifier = ">=13.0.0" },
|
|
138
|
+
{ name = "typer", specifier = ">=0.9.0" },
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
[[package]]
|
|
142
|
+
name = "pygments"
|
|
143
|
+
version = "2.19.2"
|
|
144
|
+
source = { registry = "https://pypi.org/simple" }
|
|
145
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
|
146
|
+
wheels = [
|
|
147
|
+
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
[[package]]
|
|
151
|
+
name = "rich"
|
|
152
|
+
version = "14.3.2"
|
|
153
|
+
source = { registry = "https://pypi.org/simple" }
|
|
154
|
+
dependencies = [
|
|
155
|
+
{ name = "markdown-it-py" },
|
|
156
|
+
{ name = "pygments" },
|
|
157
|
+
]
|
|
158
|
+
sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" }
|
|
159
|
+
wheels = [
|
|
160
|
+
{ url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" },
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
[[package]]
|
|
164
|
+
name = "shellingham"
|
|
165
|
+
version = "1.5.4"
|
|
166
|
+
source = { registry = "https://pypi.org/simple" }
|
|
167
|
+
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
|
168
|
+
wheels = [
|
|
169
|
+
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
[[package]]
|
|
173
|
+
name = "typer"
|
|
174
|
+
version = "0.23.1"
|
|
175
|
+
source = { registry = "https://pypi.org/simple" }
|
|
176
|
+
dependencies = [
|
|
177
|
+
{ name = "annotated-doc" },
|
|
178
|
+
{ name = "click" },
|
|
179
|
+
{ name = "rich" },
|
|
180
|
+
{ name = "shellingham" },
|
|
181
|
+
]
|
|
182
|
+
sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" }
|
|
183
|
+
wheels = [
|
|
184
|
+
{ url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" },
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
[[package]]
|
|
188
|
+
name = "typing-extensions"
|
|
189
|
+
version = "4.15.0"
|
|
190
|
+
source = { registry = "https://pypi.org/simple" }
|
|
191
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
192
|
+
wheels = [
|
|
193
|
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
194
|
+
]
|