plugsync-cli 0.1.0__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.
- plugsync_cli/__init__.py +2 -0
- plugsync_cli/bundler.py +77 -0
- plugsync_cli/client.py +201 -0
- plugsync_cli/commands/__init__.py +1 -0
- plugsync_cli/commands/auth.py +156 -0
- plugsync_cli/commands/diff.py +169 -0
- plugsync_cli/commands/log.py +104 -0
- plugsync_cli/commands/plugin.py +470 -0
- plugsync_cli/commands/preview.py +107 -0
- plugsync_cli/commands/pull.py +121 -0
- plugsync_cli/commands/push.py +101 -0
- plugsync_cli/commands/rollback.py +72 -0
- plugsync_cli/commands/validate.py +66 -0
- plugsync_cli/compat.py +73 -0
- plugsync_cli/config.py +51 -0
- plugsync_cli/main.py +84 -0
- plugsync_cli/serializer.py +205 -0
- plugsync_cli-0.1.0.dist-info/METADATA +93 -0
- plugsync_cli-0.1.0.dist-info/RECORD +22 -0
- plugsync_cli-0.1.0.dist-info/WHEEL +5 -0
- plugsync_cli-0.1.0.dist-info/entry_points.txt +2 -0
- plugsync_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Read and write local connector directory format.
|
|
2
|
+
|
|
3
|
+
Directory structure:
|
|
4
|
+
connector-name/
|
|
5
|
+
├── plugsync.yaml
|
|
6
|
+
├── entities/
|
|
7
|
+
│ ├── contacts.yaml
|
|
8
|
+
│ └── orders.yaml
|
|
9
|
+
└── handlers/
|
|
10
|
+
├── order_created.py
|
|
11
|
+
└── contact_updated.py
|
|
12
|
+
"""
|
|
13
|
+
import io
|
|
14
|
+
import re
|
|
15
|
+
import zipfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _event_type_to_filename(event_type: str) -> str:
|
|
22
|
+
"""Convert event_type like 'order.created' to filename 'order_created.py'."""
|
|
23
|
+
return re.sub(r"[^a-zA-Z0-9]", "_", event_type) + ".py"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _filename_to_event_type(filename: str) -> str:
|
|
27
|
+
"""Convert filename like 'order_created.py' to 'order.created'."""
|
|
28
|
+
stem = Path(filename).stem
|
|
29
|
+
return stem.replace("_", ".")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_entity_yaml(name: str, config: dict) -> dict:
|
|
33
|
+
"""Flatten entity config to top-level for readable YAML."""
|
|
34
|
+
result = {"name": name}
|
|
35
|
+
for key in (
|
|
36
|
+
"entity_type",
|
|
37
|
+
"hubspot_object_type",
|
|
38
|
+
"identity",
|
|
39
|
+
"field_mappings",
|
|
40
|
+
"associations",
|
|
41
|
+
"scope_overrides",
|
|
42
|
+
):
|
|
43
|
+
if key in config:
|
|
44
|
+
value = config[key]
|
|
45
|
+
if key in ("associations", "scope_overrides") and not value:
|
|
46
|
+
continue
|
|
47
|
+
result[key] = value
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_entity_yaml(data: dict) -> dict:
|
|
52
|
+
"""Reconstruct entity dict from flattened YAML."""
|
|
53
|
+
name = data.get("name", "unknown")
|
|
54
|
+
config = {}
|
|
55
|
+
for key in (
|
|
56
|
+
"entity_type",
|
|
57
|
+
"hubspot_object_type",
|
|
58
|
+
"identity",
|
|
59
|
+
"field_mappings",
|
|
60
|
+
"associations",
|
|
61
|
+
"scope_overrides",
|
|
62
|
+
):
|
|
63
|
+
if key in data:
|
|
64
|
+
config[key] = data[key]
|
|
65
|
+
return {"name": name, "config": config}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_function_name(code: str) -> str:
|
|
69
|
+
"""Extract the first function name from Python code."""
|
|
70
|
+
for line in code.split("\n"):
|
|
71
|
+
stripped = line.strip()
|
|
72
|
+
if stripped.startswith("async def "):
|
|
73
|
+
return stripped.split("(")[0].replace("async def ", "").strip()
|
|
74
|
+
if stripped.startswith("def "):
|
|
75
|
+
return stripped.split("(")[0].replace("def ", "").strip()
|
|
76
|
+
return "handler"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_handler_file(content: str) -> tuple[str | None, str]:
|
|
80
|
+
"""Parse handler file, extracting event_type from header comment."""
|
|
81
|
+
lines = content.split("\n")
|
|
82
|
+
if lines and lines[0].startswith("# event_type:"):
|
|
83
|
+
event_type = lines[0].split(":", 1)[1].strip()
|
|
84
|
+
code = "\n".join(lines[1:])
|
|
85
|
+
if code.startswith("\n"):
|
|
86
|
+
code = code[1:]
|
|
87
|
+
return event_type, code
|
|
88
|
+
return None, content
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_directory(
|
|
92
|
+
output_dir: Path,
|
|
93
|
+
manifest: dict,
|
|
94
|
+
entities: list[dict],
|
|
95
|
+
handlers: list[dict],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Write connector state as a local directory.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
output_dir: Path to the connector directory (will be created).
|
|
101
|
+
manifest: {"name": ..., "settings": {...}}
|
|
102
|
+
entities: [{"name": ..., "config": {...}}]
|
|
103
|
+
handlers: [{"event_type": ..., "function_name": ..., "code": ...}]
|
|
104
|
+
"""
|
|
105
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
# plugsync.yaml
|
|
108
|
+
with open(output_dir / "plugsync.yaml", "w") as f:
|
|
109
|
+
yaml.dump(manifest, f, default_flow_style=False, sort_keys=False)
|
|
110
|
+
|
|
111
|
+
# entities/
|
|
112
|
+
if entities:
|
|
113
|
+
entities_dir = output_dir / "entities"
|
|
114
|
+
entities_dir.mkdir(exist_ok=True)
|
|
115
|
+
for entity in entities:
|
|
116
|
+
entity_data = _build_entity_yaml(entity["name"], entity["config"])
|
|
117
|
+
with open(entities_dir / f"{entity['name']}.yaml", "w") as f:
|
|
118
|
+
yaml.dump(entity_data, f, default_flow_style=False, sort_keys=False)
|
|
119
|
+
|
|
120
|
+
# handlers/
|
|
121
|
+
if handlers:
|
|
122
|
+
handlers_dir = output_dir / "handlers"
|
|
123
|
+
handlers_dir.mkdir(exist_ok=True)
|
|
124
|
+
for handler in handlers:
|
|
125
|
+
filename = _event_type_to_filename(handler["event_type"])
|
|
126
|
+
code = handler["code"]
|
|
127
|
+
header = f"# event_type: {handler['event_type']}\n"
|
|
128
|
+
if not code.startswith("# event_type:"):
|
|
129
|
+
code = header + code
|
|
130
|
+
with open(handlers_dir / filename, "w") as f:
|
|
131
|
+
f.write(code)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def read_directory(connector_dir: Path) -> tuple[dict, list[dict], list[dict]]:
|
|
135
|
+
"""Read a local connector directory into (manifest, entities, handlers).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
manifest: dict from plugsync.yaml
|
|
139
|
+
entities: list of {"name": ..., "config": {...}}
|
|
140
|
+
handlers: list of {"event_type": ..., "function_name": ..., "code": ...}
|
|
141
|
+
"""
|
|
142
|
+
manifest_path = connector_dir / "plugsync.yaml"
|
|
143
|
+
if not manifest_path.exists():
|
|
144
|
+
raise FileNotFoundError(f"No plugsync.yaml found in {connector_dir}")
|
|
145
|
+
|
|
146
|
+
with open(manifest_path) as f:
|
|
147
|
+
manifest = yaml.safe_load(f)
|
|
148
|
+
|
|
149
|
+
entities = []
|
|
150
|
+
entities_dir = connector_dir / "entities"
|
|
151
|
+
if entities_dir.exists():
|
|
152
|
+
for yaml_file in sorted(entities_dir.glob("*.yaml")):
|
|
153
|
+
with open(yaml_file) as f:
|
|
154
|
+
data = yaml.safe_load(f)
|
|
155
|
+
entities.append(_parse_entity_yaml(data))
|
|
156
|
+
|
|
157
|
+
handlers = []
|
|
158
|
+
handlers_dir = connector_dir / "handlers"
|
|
159
|
+
if handlers_dir.exists():
|
|
160
|
+
for py_file in sorted(handlers_dir.glob("*.py")):
|
|
161
|
+
content = py_file.read_text()
|
|
162
|
+
event_type_from_header, code = _parse_handler_file(content)
|
|
163
|
+
event_type = event_type_from_header or _filename_to_event_type(py_file.name)
|
|
164
|
+
function_name = _extract_function_name(code)
|
|
165
|
+
handlers.append({
|
|
166
|
+
"event_type": event_type,
|
|
167
|
+
"function_name": function_name,
|
|
168
|
+
"code": code,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return manifest, entities, handlers
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def read_directory_to_zip(connector_dir: Path) -> bytes:
|
|
175
|
+
"""Read a local directory and pack it into a ZIP for API upload."""
|
|
176
|
+
manifest, entities, handlers = read_directory(connector_dir)
|
|
177
|
+
connector_name = manifest.get("name", connector_dir.name)
|
|
178
|
+
prefix = connector_name
|
|
179
|
+
|
|
180
|
+
buf = io.BytesIO()
|
|
181
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
182
|
+
# plugsync.yaml
|
|
183
|
+
zf.writestr(
|
|
184
|
+
f"{prefix}/plugsync.yaml",
|
|
185
|
+
yaml.dump(manifest, default_flow_style=False, sort_keys=False),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# entities
|
|
189
|
+
for entity in entities:
|
|
190
|
+
entity_data = _build_entity_yaml(entity["name"], entity["config"])
|
|
191
|
+
zf.writestr(
|
|
192
|
+
f"{prefix}/entities/{entity['name']}.yaml",
|
|
193
|
+
yaml.dump(entity_data, default_flow_style=False, sort_keys=False),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# handlers
|
|
197
|
+
for handler in handlers:
|
|
198
|
+
filename = _event_type_to_filename(handler["event_type"])
|
|
199
|
+
code = handler["code"]
|
|
200
|
+
header = f"# event_type: {handler['event_type']}\n"
|
|
201
|
+
if not code.startswith("# event_type:"):
|
|
202
|
+
code = header + code
|
|
203
|
+
zf.writestr(f"{prefix}/handlers/{filename}", code)
|
|
204
|
+
|
|
205
|
+
return buf.getvalue()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plugsync-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for plugsync HubSpot connectors -- manage connectors and plugins like code, no repo checkout required
|
|
5
|
+
Author-email: Exelab <hello@plugsync.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://plugsync.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/exelab/plugsync/blob/main/docs/content/reference/plugins/dev-loop.md
|
|
9
|
+
Project-URL: Repository, https://github.com/exelab/plugsync
|
|
10
|
+
Project-URL: Issues, https://github.com/exelab/plugsync/issues
|
|
11
|
+
Keywords: plugsync,hubspot,cli,connectors,integration
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: Other/Proprietary License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: click>=8.1
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
30
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# plugsync-cli
|
|
33
|
+
|
|
34
|
+
Command-line client for [plugsync](https://plugsync.com), managing HubSpot
|
|
35
|
+
connectors like code: pull a connector's config to a local directory, edit
|
|
36
|
+
it, push it back, diff against the remote, validate, preview a publish, and
|
|
37
|
+
manage plugins -- all without cloning the plugsync backend repo.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install plugsync-cli
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.12+.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
plugsync config set api_url https://your-plugsync-instance.example.com
|
|
51
|
+
plugsync auth login -e you@example.com
|
|
52
|
+
plugsync pull my-connector
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`plugsync auth login` saves a bearer credential to `~/.plugsync/config.yaml`
|
|
56
|
+
that every subsequent command reuses. `PLUGSYNC_API_URL` / `PLUGSYNC_API_KEY`
|
|
57
|
+
environment variables override the config file, handy for CI.
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Command | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `plugsync auth login` / `logout` | Authenticate (email/password JWT, or a long-lived org API key with `--token`) |
|
|
64
|
+
| `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. remote working state |
|
|
68
|
+
| `plugsync validate <name>` | Validate a local connector config |
|
|
69
|
+
| `plugsync preview <name>` | Preview a publish's effects |
|
|
70
|
+
| `plugsync log <name>` | Show a connector's revision history |
|
|
71
|
+
| `plugsync rollback <name>` | Restore a previous revision |
|
|
72
|
+
| `plugsync plugin init/push/list/status/logs/invoke` | Author and manage plugins (Enterprise tier) |
|
|
73
|
+
|
|
74
|
+
Run `plugsync --help` or `plugsync <command> --help` for the full option list.
|
|
75
|
+
|
|
76
|
+
## Compatibility
|
|
77
|
+
|
|
78
|
+
This CLI ships on its own release cadence, independent of the plugsync
|
|
79
|
+
backend. It declares the minimum API version it expects and checks it
|
|
80
|
+
against the API at the start of any command that talks to it, warning
|
|
81
|
+
(never failing outright) if the API is older than expected.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
Full reference docs, including the plugin authoring dev-loop, live at
|
|
86
|
+
[docs.plugsync.com](https://plugsync.com) and in the
|
|
87
|
+
[plugsync repository](https://github.com/exelab/plugsync/tree/main/docs).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
Proprietary. This CLI talks to the plugsync SaaS API; see
|
|
92
|
+
[plugsync.com/terms](https://plugsync.com/terms) for the terms governing use
|
|
93
|
+
of the plugsync service.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
plugsync_cli/__init__.py,sha256=KNokQtEIi9Xm18J2kpSGo7zpzEiESULyERWUNoWy4AI,75
|
|
2
|
+
plugsync_cli/bundler.py,sha256=nCskRqAILHe3LLMZSyS4klBbM879yBvVJODpZvEEm38,2219
|
|
3
|
+
plugsync_cli/client.py,sha256=HNOjHoIGRkMd5V9gCEiVRhdFoUjJqK7qJJt6ppVsqT8,7620
|
|
4
|
+
plugsync_cli/compat.py,sha256=J0kswv2hTv_SoD9ibKb-fVvN0fHzdWwuJYUCA8553IQ,2841
|
|
5
|
+
plugsync_cli/config.py,sha256=WX4Db5K39oWePiWxwnhtEfSDkF2_QqJZ4IL-Su0lxJU,1517
|
|
6
|
+
plugsync_cli/main.py,sha256=O5oMHFQAeKwmFqc4fwWhTYa7p005eC2O6u4IEa-ss5c,2209
|
|
7
|
+
plugsync_cli/serializer.py,sha256=OjatMuD1nkg6oomBWXB8599dqpgaj4zboh1t8hiLqTU,6912
|
|
8
|
+
plugsync_cli/commands/__init__.py,sha256=wgYTWnZKXF-4M9ITNDhZMTUJH6_FthcUt_5zGj9NS8I,20
|
|
9
|
+
plugsync_cli/commands/auth.py,sha256=8M2N1E6KEBdNjcARDFWSzk4QCimOyeozam8gdfsxCw4,5435
|
|
10
|
+
plugsync_cli/commands/diff.py,sha256=ubbyKBdFMlf0iue3WjlS2spS36EHyVPK7bWXVhLy7jQ,6621
|
|
11
|
+
plugsync_cli/commands/log.py,sha256=49GBgAvvs07d-OwMEgKKc7WalCKH2xmr5l7Ov6N13jM,3376
|
|
12
|
+
plugsync_cli/commands/plugin.py,sha256=ZNku_q4kQ_Aefz1EKysTeglJi74cAtX53FfB7VWUWuw,15715
|
|
13
|
+
plugsync_cli/commands/preview.py,sha256=J7dMryWH0lzGgXPfUW6fVTMQWFqv9QPQv7Rz2Z0A6JI,3122
|
|
14
|
+
plugsync_cli/commands/pull.py,sha256=R-Lu-M__M3DpKyS7Cn3Jzvg7HSE9cJ2ZRXmD9qxqj2Q,4568
|
|
15
|
+
plugsync_cli/commands/push.py,sha256=zwh_dj6xwGNItDKN0VWVR8slMhCjCMnagsavDLcjPzM,3545
|
|
16
|
+
plugsync_cli/commands/rollback.py,sha256=3KM3WnPAx7fVnQOgaLlUQu5A3lXuYAxP1tvL44lQTsA,2354
|
|
17
|
+
plugsync_cli/commands/validate.py,sha256=5pCdXXeiFbNw8IyeGYnAN0EJIQx0wzNuDt1wCc3i-kE,1977
|
|
18
|
+
plugsync_cli-0.1.0.dist-info/METADATA,sha256=U6wJg7GBWdnDG2j3sPp10q9uc3ydPzNvUYbazC1MXS8,3667
|
|
19
|
+
plugsync_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
20
|
+
plugsync_cli-0.1.0.dist-info/entry_points.txt,sha256=qWyWFQxY9W7L0hEwoDFjQjgs7TxJMsETk2nIe8jCA9E,51
|
|
21
|
+
plugsync_cli-0.1.0.dist-info/top_level.txt,sha256=tShdp15OlzUttfE_QlFMLIfwQfImdp4YVhmB5rXZCgA,13
|
|
22
|
+
plugsync_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plugsync_cli
|