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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ plugsync = plugsync_cli.main:cli
@@ -0,0 +1 @@
1
+ plugsync_cli