plugsync-cli 0.1.1__tar.gz → 0.4.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.
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/PKG-INFO +20 -14
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/README.md +19 -13
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/__init__.py +1 -1
- plugsync_cli-0.4.0/plugsync_cli/client.py +351 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/auth.py +4 -0
- plugsync_cli-0.4.0/plugsync_cli/commands/diff.py +439 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/plugin.py +94 -7
- plugsync_cli-0.4.0/plugsync_cli/commands/preview.py +68 -0
- plugsync_cli-0.4.0/plugsync_cli/commands/pull.py +70 -0
- plugsync_cli-0.4.0/plugsync_cli/commands/push.py +140 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/rollback.py +7 -7
- plugsync_cli-0.4.0/plugsync_cli/commands/validate.py +54 -0
- plugsync_cli-0.4.0/plugsync_cli/compat.py +96 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/config.py +12 -2
- plugsync_cli-0.4.0/plugsync_cli/context.py +205 -0
- plugsync_cli-0.4.0/plugsync_cli/findings.py +119 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/main.py +46 -3
- plugsync_cli-0.4.0/plugsync_cli/schema_defaults.py +183 -0
- plugsync_cli-0.4.0/plugsync_cli/serializer.py +426 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/PKG-INFO +20 -14
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/SOURCES.txt +8 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/pyproject.toml +1 -1
- plugsync_cli-0.4.0/tests/test_api_contract.py +274 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_auth_commands.py +26 -0
- plugsync_cli-0.4.0/tests/test_commands.py +1482 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_compat.py +25 -13
- plugsync_cli-0.4.0/tests/test_config.py +93 -0
- plugsync_cli-0.4.0/tests/test_e2e_draft_api.py +593 -0
- plugsync_cli-0.4.0/tests/test_hand_edited_yaml.py +341 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_plugin_commands.py +328 -0
- plugsync_cli-0.4.0/tests/test_schema_defaults.py +179 -0
- plugsync_cli-0.4.0/tests/test_serializer.py +239 -0
- plugsync_cli-0.1.1/plugsync_cli/client.py +0 -201
- plugsync_cli-0.1.1/plugsync_cli/commands/diff.py +0 -169
- plugsync_cli-0.1.1/plugsync_cli/commands/preview.py +0 -107
- plugsync_cli-0.1.1/plugsync_cli/commands/pull.py +0 -121
- plugsync_cli-0.1.1/plugsync_cli/commands/push.py +0 -101
- plugsync_cli-0.1.1/plugsync_cli/commands/validate.py +0 -66
- plugsync_cli-0.1.1/plugsync_cli/compat.py +0 -73
- plugsync_cli-0.1.1/plugsync_cli/serializer.py +0 -205
- plugsync_cli-0.1.1/tests/test_commands.py +0 -339
- plugsync_cli-0.1.1/tests/test_serializer.py +0 -160
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/bundler.py +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/__init__.py +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli/commands/log.py +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/dependency_links.txt +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/entry_points.txt +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/requires.txt +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/plugsync_cli.egg-info/top_level.txt +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/setup.cfg +0 -0
- {plugsync_cli-0.1.1 → plugsync_cli-0.4.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plugsync-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Command-line client for plugsync HubSpot connectors -- manage connectors and plugins like code, no repo checkout required
|
|
5
5
|
Author-email: Exelab <hello@plugsync.com>
|
|
6
6
|
License: Proprietary
|
|
@@ -33,8 +33,12 @@ Requires-Dist: respx>=0.21; extra == "dev"
|
|
|
33
33
|
|
|
34
34
|
Command-line client for [plugsync](https://plugsync.com), managing HubSpot
|
|
35
35
|
connectors like code: pull a connector's config to a local directory, edit
|
|
36
|
-
it, push it back
|
|
37
|
-
manage plugins -- all without cloning the plugsync
|
|
36
|
+
it, push it back to the draft, diff against a published version, validate,
|
|
37
|
+
preview a publish, and manage plugins -- all without cloning the plugsync
|
|
38
|
+
backend repo.
|
|
39
|
+
|
|
40
|
+
`push` writes the draft and stops. Publishing is opt-in (`push --publish`),
|
|
41
|
+
so a config never reaches production as a side effect of saving a file.
|
|
38
42
|
|
|
39
43
|
## Install
|
|
40
44
|
|
|
@@ -47,14 +51,16 @@ Requires Python 3.12+.
|
|
|
47
51
|
## Quickstart
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
plugsync config set api_url https://app.plugsync.com
|
|
51
54
|
plugsync auth login -e you@example.com
|
|
52
55
|
plugsync pull my-connector
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
`api_url` defaults to the hosted platform (`https://app.plugsync.com`), so
|
|
59
|
+
there's nothing to configure for the common case. Point it elsewhere with
|
|
60
|
+
`plugsync config set api_url <url>`. `plugsync auth login` saves a bearer
|
|
61
|
+
credential to `~/.plugsync/config.yaml` that every subsequent command
|
|
62
|
+
reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY` environment variables
|
|
63
|
+
override the config file, handy for CI.
|
|
58
64
|
|
|
59
65
|
## Commands
|
|
60
66
|
|
|
@@ -62,13 +68,13 @@ environment variables override the config file, handy for CI.
|
|
|
62
68
|
|---|---|
|
|
63
69
|
| `plugsync auth login` / `logout` | Authenticate (email/password JWT, or a long-lived org API key with `--token`) |
|
|
64
70
|
| `plugsync config set/get/show` | Manage `~/.plugsync/config.yaml` |
|
|
65
|
-
| `plugsync pull <name>` | Download a connector as a local directory |
|
|
66
|
-
| `plugsync push <name>` | Upload a local connector directory |
|
|
67
|
-
| `plugsync diff <name>` | Diff local vs.
|
|
68
|
-
| `plugsync validate <name>` |
|
|
69
|
-
| `plugsync preview <name>` | Preview a publish's effects |
|
|
70
|
-
| `plugsync log <name>` | Show a connector's
|
|
71
|
-
| `plugsync rollback <name>` | Restore a
|
|
71
|
+
| `plugsync pull <name>` | Download a connector's draft as a local directory (`--revision N` for a published version) |
|
|
72
|
+
| `plugsync push <name>` | Upload a local connector directory to the draft (`--publish` to also publish) |
|
|
73
|
+
| `plugsync diff <name>` | Diff local files vs. a published version |
|
|
74
|
+
| `plugsync validate <name>` | Preflight the draft; non-zero exit on an error |
|
|
75
|
+
| `plugsync preview <name>` | Preview a publish's effects and its preflight findings |
|
|
76
|
+
| `plugsync log <name>` | Show a connector's published version history |
|
|
77
|
+
| `plugsync rollback <name>` | Restore a published version into the draft |
|
|
72
78
|
| `plugsync plugin init/push/list/status/logs/metrics/invoke/promote/publish/rollback` | Author and manage plugins (Enterprise tier) |
|
|
73
79
|
|
|
74
80
|
Run `plugsync --help` or `plugsync <command> --help` for the full option list.
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Command-line client for [plugsync](https://plugsync.com), managing HubSpot
|
|
4
4
|
connectors like code: pull a connector's config to a local directory, edit
|
|
5
|
-
it, push it back
|
|
6
|
-
manage plugins -- all without cloning the plugsync
|
|
5
|
+
it, push it back to the draft, diff against a published version, validate,
|
|
6
|
+
preview a publish, and manage plugins -- all without cloning the plugsync
|
|
7
|
+
backend repo.
|
|
8
|
+
|
|
9
|
+
`push` writes the draft and stops. Publishing is opt-in (`push --publish`),
|
|
10
|
+
so a config never reaches production as a side effect of saving a file.
|
|
7
11
|
|
|
8
12
|
## Install
|
|
9
13
|
|
|
@@ -16,14 +20,16 @@ Requires Python 3.12+.
|
|
|
16
20
|
## Quickstart
|
|
17
21
|
|
|
18
22
|
```bash
|
|
19
|
-
plugsync config set api_url https://app.plugsync.com
|
|
20
23
|
plugsync auth login -e you@example.com
|
|
21
24
|
plugsync pull my-connector
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
`
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
`api_url` defaults to the hosted platform (`https://app.plugsync.com`), so
|
|
28
|
+
there's nothing to configure for the common case. Point it elsewhere with
|
|
29
|
+
`plugsync config set api_url <url>`. `plugsync auth login` saves a bearer
|
|
30
|
+
credential to `~/.plugsync/config.yaml` that every subsequent command
|
|
31
|
+
reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY` environment variables
|
|
32
|
+
override the config file, handy for CI.
|
|
27
33
|
|
|
28
34
|
## Commands
|
|
29
35
|
|
|
@@ -31,13 +37,13 @@ environment variables override the config file, handy for CI.
|
|
|
31
37
|
|---|---|
|
|
32
38
|
| `plugsync auth login` / `logout` | Authenticate (email/password JWT, or a long-lived org API key with `--token`) |
|
|
33
39
|
| `plugsync config set/get/show` | Manage `~/.plugsync/config.yaml` |
|
|
34
|
-
| `plugsync pull <name>` | Download a connector as a local directory |
|
|
35
|
-
| `plugsync push <name>` | Upload a local connector directory |
|
|
36
|
-
| `plugsync diff <name>` | Diff local vs.
|
|
37
|
-
| `plugsync validate <name>` |
|
|
38
|
-
| `plugsync preview <name>` | Preview a publish's effects |
|
|
39
|
-
| `plugsync log <name>` | Show a connector's
|
|
40
|
-
| `plugsync rollback <name>` | Restore a
|
|
40
|
+
| `plugsync pull <name>` | Download a connector's draft as a local directory (`--revision N` for a published version) |
|
|
41
|
+
| `plugsync push <name>` | Upload a local connector directory to the draft (`--publish` to also publish) |
|
|
42
|
+
| `plugsync diff <name>` | Diff local files vs. a published version |
|
|
43
|
+
| `plugsync validate <name>` | Preflight the draft; non-zero exit on an error |
|
|
44
|
+
| `plugsync preview <name>` | Preview a publish's effects and its preflight findings |
|
|
45
|
+
| `plugsync log <name>` | Show a connector's published version history |
|
|
46
|
+
| `plugsync rollback <name>` | Restore a published version into the draft |
|
|
41
47
|
| `plugsync plugin init/push/list/status/logs/metrics/invoke/promote/publish/rollback` | Author and manage plugins (Enterprise tier) |
|
|
42
48
|
|
|
43
49
|
Run `plugsync --help` or `plugsync <command> --help` for the full option list.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""PluSync CLI — git-like connector management."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""HTTP client wrapper for PluSync REST API."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from plugsync_cli.compat import check_api_compat
|
|
7
|
+
from plugsync_cli.config import get_api_key, get_api_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DraftConflict(Exception):
|
|
11
|
+
"""The draft moved on since the version the caller read.
|
|
12
|
+
|
|
13
|
+
Carries the draft version the server currently holds, so a command can tell
|
|
14
|
+
the user what to re-pull instead of surfacing a bare 409.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, current_version: int | None):
|
|
18
|
+
self.current_version = current_version
|
|
19
|
+
super().__init__(
|
|
20
|
+
"the connector draft changed since it was pulled"
|
|
21
|
+
+ (f" (server is at draft v{current_version})" if current_version else "")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _render_detail(detail) -> str:
|
|
26
|
+
"""The backend's `detail` as one readable block.
|
|
27
|
+
|
|
28
|
+
FastAPI answers a string for a hand-raised HTTPException, a dict for the
|
|
29
|
+
structured ones (draft conflicts, upgrade payloads), and a list of
|
|
30
|
+
per-field entries for a request-validation failure. The last one carries
|
|
31
|
+
the precise Pydantic error, which is exactly what an operator who mistyped
|
|
32
|
+
a config key needs to see.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(detail, str):
|
|
35
|
+
return detail
|
|
36
|
+
if isinstance(detail, list):
|
|
37
|
+
lines = []
|
|
38
|
+
for entry in detail:
|
|
39
|
+
if isinstance(entry, dict) and "msg" in entry:
|
|
40
|
+
location = ".".join(str(part) for part in entry.get("loc", []))
|
|
41
|
+
lines.append(f"{location}: {entry['msg']}" if location else entry["msg"])
|
|
42
|
+
else:
|
|
43
|
+
lines.append(json.dumps(entry, ensure_ascii=False))
|
|
44
|
+
return "\n ".join([""] + lines)
|
|
45
|
+
return json.dumps(detail, ensure_ascii=False, indent=2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def describe_http_error(exc: httpx.HTTPStatusError) -> str:
|
|
49
|
+
"""A non-2xx turned into something an operator can act on.
|
|
50
|
+
|
|
51
|
+
Without this the CLI dies on the raw `raise_for_status()` traceback and
|
|
52
|
+
throws away the response body, which is where the backend says what is
|
|
53
|
+
actually wrong.
|
|
54
|
+
"""
|
|
55
|
+
response, request = exc.response, exc.request
|
|
56
|
+
summary = (
|
|
57
|
+
f"{request.method} {request.url.path} failed "
|
|
58
|
+
f"(HTTP {response.status_code} {response.reason_phrase})"
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
payload = response.json()
|
|
62
|
+
except ValueError:
|
|
63
|
+
body = (response.text or "").strip()
|
|
64
|
+
return f"{summary}: {body}" if body else summary
|
|
65
|
+
|
|
66
|
+
detail = payload.get("detail", payload) if isinstance(payload, dict) else payload
|
|
67
|
+
return f"{summary}: {_render_detail(detail)}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _conflict_version(response: httpx.Response) -> int | None:
|
|
71
|
+
"""The `current_version` the draft API reports in its 409 detail."""
|
|
72
|
+
try:
|
|
73
|
+
detail = response.json().get("detail")
|
|
74
|
+
except ValueError:
|
|
75
|
+
return None
|
|
76
|
+
return detail.get("current_version") if isinstance(detail, dict) else None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PlugSyncClient:
|
|
80
|
+
"""Thin wrapper around httpx for PluSync API calls."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, api_url: str | None = None, api_key: str | None = None):
|
|
83
|
+
self.api_url = api_url or get_api_url()
|
|
84
|
+
self.api_key = api_key or get_api_key()
|
|
85
|
+
if not self.api_key:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
"No API key configured. Run 'plugsync config set api_key <key>' "
|
|
88
|
+
"or set PLUGSYNC_API_KEY environment variable."
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
check_api_compat(self.api_url)
|
|
92
|
+
except Exception:
|
|
93
|
+
# Compat check is advisory only (issue #957): never let it
|
|
94
|
+
# block a command from running, even if it misbehaves.
|
|
95
|
+
pass
|
|
96
|
+
self.headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
97
|
+
|
|
98
|
+
def _url(self, path: str) -> str:
|
|
99
|
+
return f"{self.api_url}/api{path}"
|
|
100
|
+
|
|
101
|
+
def _client(self) -> httpx.Client:
|
|
102
|
+
return httpx.Client(headers=self.headers, timeout=60.0)
|
|
103
|
+
|
|
104
|
+
def list_connectors(self) -> list[dict]:
|
|
105
|
+
with self._client() as c:
|
|
106
|
+
r = c.get(self._url("/connectors"))
|
|
107
|
+
r.raise_for_status()
|
|
108
|
+
return r.json()
|
|
109
|
+
|
|
110
|
+
def find_connector(self, name: str) -> dict | None:
|
|
111
|
+
connectors = self.list_connectors()
|
|
112
|
+
for conn in connectors:
|
|
113
|
+
if conn["name"] == name:
|
|
114
|
+
return conn
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def get_connector(self, connector_id: str) -> dict:
|
|
118
|
+
with self._client() as c:
|
|
119
|
+
r = c.get(self._url(f"/connectors/{connector_id}"))
|
|
120
|
+
r.raise_for_status()
|
|
121
|
+
return r.json()
|
|
122
|
+
|
|
123
|
+
def get_draft(self, connector_id: str) -> dict:
|
|
124
|
+
"""The connector's editable config (backend DraftResponse).
|
|
125
|
+
|
|
126
|
+
The draft is the CLI's read AND write surface for the current config:
|
|
127
|
+
there is no server-side export, the config travels as JSON and the
|
|
128
|
+
directory format is produced locally by `serializer.py` (#1241).
|
|
129
|
+
"""
|
|
130
|
+
with self._client() as c:
|
|
131
|
+
r = c.get(self._url(f"/connectors/{connector_id}/draft"))
|
|
132
|
+
r.raise_for_status()
|
|
133
|
+
return r.json()
|
|
134
|
+
|
|
135
|
+
def put_draft(
|
|
136
|
+
self, connector_id: str, config: dict, expected_version: int | None = None
|
|
137
|
+
) -> dict:
|
|
138
|
+
"""Replace the whole draft config. Never publishes.
|
|
139
|
+
|
|
140
|
+
`expected_version` is the `draft_version` the caller last read: the
|
|
141
|
+
backend rejects the write with 409 if the draft moved on since, which
|
|
142
|
+
is what keeps a `pull` -> edit -> `push` cycle from silently discarding
|
|
143
|
+
someone else's dashboard edit. Raises `DraftConflict` on that 409.
|
|
144
|
+
"""
|
|
145
|
+
params = {} if expected_version is None else {"expected_version": expected_version}
|
|
146
|
+
with self._client() as c:
|
|
147
|
+
r = c.put(
|
|
148
|
+
self._url(f"/connectors/{connector_id}/draft"),
|
|
149
|
+
json=config,
|
|
150
|
+
params=params,
|
|
151
|
+
)
|
|
152
|
+
if r.status_code == 409:
|
|
153
|
+
raise DraftConflict(_conflict_version(r))
|
|
154
|
+
r.raise_for_status()
|
|
155
|
+
return r.json()
|
|
156
|
+
|
|
157
|
+
def config_surface(self) -> dict:
|
|
158
|
+
"""The config catalogue: actions, transforms, when, sources, json_schema.
|
|
159
|
+
|
|
160
|
+
`json_schema` is `ConnectorConfigV3.model_json_schema()`, defaults
|
|
161
|
+
included: it is how `plugsync diff` knows which values the server wrote
|
|
162
|
+
from a schema default rather than the operator (#1259). Org-scoped
|
|
163
|
+
auth only -- the catalogue itself is the same for every org.
|
|
164
|
+
"""
|
|
165
|
+
with self._client() as c:
|
|
166
|
+
r = c.get(self._url("/config-surface"))
|
|
167
|
+
r.raise_for_status()
|
|
168
|
+
return r.json()
|
|
169
|
+
|
|
170
|
+
def get_version(self, connector_id: str, version: int) -> dict:
|
|
171
|
+
"""One published config version, config included (backend VersionResponse)."""
|
|
172
|
+
with self._client() as c:
|
|
173
|
+
r = c.get(self._url(f"/connectors/{connector_id}/versions/{version}"))
|
|
174
|
+
r.raise_for_status()
|
|
175
|
+
return r.json()
|
|
176
|
+
|
|
177
|
+
def preflight(self, connector_id: str) -> dict:
|
|
178
|
+
"""Preflight findings on the draft, split into errors/warnings/infos."""
|
|
179
|
+
with self._client() as c:
|
|
180
|
+
r = c.get(self._url(f"/connectors/{connector_id}/preflight"))
|
|
181
|
+
r.raise_for_status()
|
|
182
|
+
return r.json()
|
|
183
|
+
|
|
184
|
+
def preview(self, connector_id: str) -> dict:
|
|
185
|
+
"""What publishing the draft would change against the active version."""
|
|
186
|
+
with self._client() as c:
|
|
187
|
+
r = c.get(self._url(f"/connectors/{connector_id}/preview"))
|
|
188
|
+
r.raise_for_status()
|
|
189
|
+
return r.json()
|
|
190
|
+
|
|
191
|
+
def publish(
|
|
192
|
+
self, connector_id: str, change_summary: str | None = None, force: bool = False,
|
|
193
|
+
) -> dict:
|
|
194
|
+
"""`force` (#1367, DX-61): publish a new version even when the draft
|
|
195
|
+
is identical to the active one - the values a publish resolves (plugin
|
|
196
|
+
ARN/version, HubSpot schema) can drift without any draft edit. Unrelated
|
|
197
|
+
to `plugsync push --force`, which discards a concurrent DRAFT edit."""
|
|
198
|
+
with self._client() as c:
|
|
199
|
+
body = {}
|
|
200
|
+
if change_summary:
|
|
201
|
+
body["change_summary"] = change_summary
|
|
202
|
+
if force:
|
|
203
|
+
body["force"] = True
|
|
204
|
+
r = c.post(self._url(f"/connectors/{connector_id}/publish"), json=body)
|
|
205
|
+
r.raise_for_status()
|
|
206
|
+
return r.json()
|
|
207
|
+
|
|
208
|
+
def list_revisions(self, connector_id: str) -> list[dict]:
|
|
209
|
+
"""Published config versions, newest first (backend: app/api/versions.py).
|
|
210
|
+
|
|
211
|
+
The endpoint is `/versions`: Plan 6a replaced the `/revisions` API the
|
|
212
|
+
CLI used to call, which is why `plugsync log` answered 404 (#1244).
|
|
213
|
+
"""
|
|
214
|
+
with self._client() as c:
|
|
215
|
+
r = c.get(self._url(f"/connectors/{connector_id}/versions"))
|
|
216
|
+
r.raise_for_status()
|
|
217
|
+
return r.json()
|
|
218
|
+
|
|
219
|
+
def restore_revision(self, connector_id: str, version: int) -> dict:
|
|
220
|
+
"""Copy a published version back into the draft, returning the draft.
|
|
221
|
+
|
|
222
|
+
Does not publish: the restored config becomes the connector's draft and
|
|
223
|
+
needs an explicit publish to go live.
|
|
224
|
+
"""
|
|
225
|
+
with self._client() as c:
|
|
226
|
+
r = c.post(self._url(f"/connectors/{connector_id}/versions/{version}/restore"))
|
|
227
|
+
r.raise_for_status()
|
|
228
|
+
return r.json()
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Plugin API (T10, issue #190)
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def plugin_create(self, name: str, capabilities: list[str] | None = None) -> dict:
|
|
235
|
+
"""Create a new plugin (status=draft)."""
|
|
236
|
+
body: dict = {"name": name}
|
|
237
|
+
if capabilities is not None:
|
|
238
|
+
body["capabilities"] = capabilities
|
|
239
|
+
with self._client() as c:
|
|
240
|
+
r = c.post(self._url("/plugins"), json=body)
|
|
241
|
+
r.raise_for_status()
|
|
242
|
+
return r.json()
|
|
243
|
+
|
|
244
|
+
def plugin_list(self) -> list[dict]:
|
|
245
|
+
"""List all plugins for the current org."""
|
|
246
|
+
with self._client() as c:
|
|
247
|
+
r = c.get(self._url("/plugins"))
|
|
248
|
+
r.raise_for_status()
|
|
249
|
+
return r.json()
|
|
250
|
+
|
|
251
|
+
def plugin_get(self, plugin_id: str) -> dict:
|
|
252
|
+
"""Get a single plugin by ID."""
|
|
253
|
+
with self._client() as c:
|
|
254
|
+
r = c.get(self._url(f"/plugins/{plugin_id}"))
|
|
255
|
+
r.raise_for_status()
|
|
256
|
+
return r.json()
|
|
257
|
+
|
|
258
|
+
def plugin_push(self, plugin_id: str, bundle_bytes: bytes, source_code: str) -> dict:
|
|
259
|
+
"""Upload a new bundle + source_code for a plugin (multipart/form-data)."""
|
|
260
|
+
with self._client() as c:
|
|
261
|
+
r = c.post(
|
|
262
|
+
self._url(f"/plugins/{plugin_id}/push"),
|
|
263
|
+
files={"bundle": ("plugin.js", bundle_bytes, "application/javascript")},
|
|
264
|
+
data={"source_code": source_code},
|
|
265
|
+
)
|
|
266
|
+
r.raise_for_status()
|
|
267
|
+
return r.json()
|
|
268
|
+
|
|
269
|
+
def plugin_promote(self, plugin_id: str, target: str) -> dict:
|
|
270
|
+
"""Promote a plugin to staging or live."""
|
|
271
|
+
with self._client() as c:
|
|
272
|
+
r = c.post(
|
|
273
|
+
self._url(f"/plugins/{plugin_id}/promote"),
|
|
274
|
+
json={"target": target},
|
|
275
|
+
)
|
|
276
|
+
r.raise_for_status()
|
|
277
|
+
return r.json()
|
|
278
|
+
|
|
279
|
+
def plugin_invoke(
|
|
280
|
+
self,
|
|
281
|
+
plugin_id: str,
|
|
282
|
+
payload: dict,
|
|
283
|
+
config: dict | None = None,
|
|
284
|
+
auth_credential: str | None = None,
|
|
285
|
+
auth_key: str | None = None,
|
|
286
|
+
) -> dict:
|
|
287
|
+
"""Proxy an invocation to the plugin's Lambda function.
|
|
288
|
+
|
|
289
|
+
`payload` is the event BODY only -- it lands at `event.payload` on
|
|
290
|
+
the Lambda side, not the whole PluginPayload envelope (issue #1365).
|
|
291
|
+
`config` mirrors `event.config`.
|
|
292
|
+
|
|
293
|
+
`auth_credential` is a name, never a secret in the clear: the server
|
|
294
|
+
reads it as an event source name and resolves that source's outbound
|
|
295
|
+
auth exactly as a real flow does (its credential, auth_type and
|
|
296
|
+
auth_header), falling back to a stored credential name when the org
|
|
297
|
+
has no source called that. The event.auth KEY those headers land
|
|
298
|
+
under is `auth_key` when given, else `auth_credential` itself; that
|
|
299
|
+
key is also the `auth` capability grant the server requires on the
|
|
300
|
+
plugin, mirroring what the runtime hands a plugin.
|
|
301
|
+
"""
|
|
302
|
+
with self._client() as c:
|
|
303
|
+
r = c.post(
|
|
304
|
+
self._url(f"/plugins/{plugin_id}/invoke"),
|
|
305
|
+
json={
|
|
306
|
+
"payload": payload,
|
|
307
|
+
"config": config or {},
|
|
308
|
+
"auth_credential": auth_credential,
|
|
309
|
+
"auth_key": auth_key,
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
r.raise_for_status()
|
|
313
|
+
return r.json()
|
|
314
|
+
|
|
315
|
+
def plugin_logs(
|
|
316
|
+
self,
|
|
317
|
+
plugin_id: str,
|
|
318
|
+
connector_id: str | None = None,
|
|
319
|
+
since_minutes: int | None = None,
|
|
320
|
+
limit: int | None = None,
|
|
321
|
+
) -> dict:
|
|
322
|
+
"""Tail the most recent CloudWatch log lines for a plugin.
|
|
323
|
+
|
|
324
|
+
Optional filters (#293): connector_id narrows to one connector's
|
|
325
|
+
lines, since_minutes widens the lookback, limit caps the line count.
|
|
326
|
+
"""
|
|
327
|
+
params: dict = {}
|
|
328
|
+
if connector_id is not None:
|
|
329
|
+
params["connector_id"] = connector_id
|
|
330
|
+
if since_minutes is not None:
|
|
331
|
+
params["since_minutes"] = since_minutes
|
|
332
|
+
if limit is not None:
|
|
333
|
+
params["limit"] = limit
|
|
334
|
+
with self._client() as c:
|
|
335
|
+
r = c.get(self._url(f"/plugins/{plugin_id}/logs"), params=params)
|
|
336
|
+
r.raise_for_status()
|
|
337
|
+
return r.json()
|
|
338
|
+
|
|
339
|
+
def plugin_metrics(self, plugin_id: str, window: str = "24h") -> dict:
|
|
340
|
+
"""CloudWatch metrics (invocations, errors, latency) for a plugin (#293)."""
|
|
341
|
+
with self._client() as c:
|
|
342
|
+
r = c.get(
|
|
343
|
+
self._url(f"/plugins/{plugin_id}/metrics"),
|
|
344
|
+
params={"window": window},
|
|
345
|
+
)
|
|
346
|
+
r.raise_for_status()
|
|
347
|
+
return r.json()
|
|
348
|
+
|
|
349
|
+
def plugin_status(self, plugin_id: str) -> dict:
|
|
350
|
+
"""Get the current status of a plugin."""
|
|
351
|
+
return self.plugin_get(plugin_id)
|
|
@@ -125,6 +125,10 @@ def auth_login(email: str | None, password: str | None, token: str | None):
|
|
|
125
125
|
raise SystemExit(1)
|
|
126
126
|
except httpx.RequestError as exc:
|
|
127
127
|
console.print(f"[red]Could not reach {api_url}: {exc}[/red]")
|
|
128
|
+
console.print(
|
|
129
|
+
"[yellow]Is this the right API URL? Set it with "
|
|
130
|
+
"'plugsync config set api_url <url>'.[/yellow]"
|
|
131
|
+
)
|
|
128
132
|
raise SystemExit(1)
|
|
129
133
|
|
|
130
134
|
data = response.json()
|