marcopolo-sdk 0.1.1__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.
- marcopolo_sdk-0.1.1/.env.example +3 -0
- marcopolo_sdk-0.1.1/.github/workflows/package-check.yml +35 -0
- marcopolo_sdk-0.1.1/.github/workflows/publish-pypi.yml +56 -0
- marcopolo_sdk-0.1.1/.gitignore +14 -0
- marcopolo_sdk-0.1.1/PKG-INFO +503 -0
- marcopolo_sdk-0.1.1/PYPI_PUBLISHING.md +74 -0
- marcopolo_sdk-0.1.1/README.md +479 -0
- marcopolo_sdk-0.1.1/pyproject.toml +59 -0
- marcopolo_sdk-0.1.1/src/marcopolo/__init__.py +29 -0
- marcopolo_sdk-0.1.1/src/marcopolo/_version.py +3 -0
- marcopolo_sdk-0.1.1/src/marcopolo/client.py +121 -0
- marcopolo_sdk-0.1.1/src/marcopolo/execution.py +267 -0
- marcopolo_sdk-0.1.1/src/marcopolo/mcp_transport.py +109 -0
- marcopolo_sdk-0.1.1/src/marcopolo/py.typed +1 -0
- marcopolo_sdk-0.1.1/src/marcopolo/query_files.py +170 -0
- marcopolo_sdk-0.1.1/tests/__init__.py +1 -0
- marcopolo_sdk-0.1.1/tests/conftest.py +3 -0
- marcopolo_sdk-0.1.1/tests/live_support.py +56 -0
- marcopolo_sdk-0.1.1/tests/test_integration_reads.py +147 -0
- marcopolo_sdk-0.1.1/tests/test_integration_salesforce_writes.py +104 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Package Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
package-check:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Check out repository
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
python -m pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Lint
|
|
29
|
+
run: ruff check .
|
|
30
|
+
|
|
31
|
+
- name: Build distributions
|
|
32
|
+
run: python -m build
|
|
33
|
+
|
|
34
|
+
- name: Validate distribution metadata
|
|
35
|
+
run: python -m twine check dist/*
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types:
|
|
6
|
+
- published
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Check out repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.11"
|
|
21
|
+
|
|
22
|
+
- name: Install build dependencies
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade pip
|
|
25
|
+
python -m pip install build twine
|
|
26
|
+
|
|
27
|
+
- name: Build distributions
|
|
28
|
+
run: python -m build
|
|
29
|
+
|
|
30
|
+
- name: Validate distribution metadata
|
|
31
|
+
run: python -m twine check dist/*
|
|
32
|
+
|
|
33
|
+
- name: Upload distributions
|
|
34
|
+
uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: python-package-distributions
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment:
|
|
43
|
+
name: pypi
|
|
44
|
+
url: https://pypi.org/p/marcopolo-sdk
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- name: Download distributions
|
|
50
|
+
uses: actions/download-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
- name: Publish distributions to PyPI
|
|
56
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marcopolo-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for executing MarcoPolo MCP data operations.
|
|
5
|
+
Project-URL: Homepage, https://github.com/immersa-co/marcopolo-python-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/immersa-co/marcopolo-python-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/immersa-co/marcopolo-python-sdk/issues
|
|
8
|
+
Author: Immersa
|
|
9
|
+
License: Proprietary
|
|
10
|
+
Keywords: client,data,marcopolo,mcp
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: mcp<2,>=1.28.1
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: build>=1.2.2; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: twine>=5.1.1; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# MarcoPolo Client
|
|
26
|
+
|
|
27
|
+
Python client library for executing governed MarcoPolo MCP connection
|
|
28
|
+
operations from application code.
|
|
29
|
+
|
|
30
|
+
The initial public surface is intentionally small:
|
|
31
|
+
|
|
32
|
+
- one async metadata API: `list_connections()`
|
|
33
|
+
- one async top-level API: `execute()`
|
|
34
|
+
- one async lower-level helper: `execute_query_file()`
|
|
35
|
+
- internal handling for remote query-file authoring under
|
|
36
|
+
`connections/<connection_name>/queries/`
|
|
37
|
+
|
|
38
|
+
The low-level MCP transport uses the official Python `mcp` SDK.
|
|
39
|
+
|
|
40
|
+
## Status
|
|
41
|
+
|
|
42
|
+
This repository implements the approved first cut of the client:
|
|
43
|
+
|
|
44
|
+
- async connection discovery
|
|
45
|
+
- async-only execution
|
|
46
|
+
- required `context`
|
|
47
|
+
- caller-provided `query_name`
|
|
48
|
+
- syntax-agnostic payload handling
|
|
49
|
+
- canonical execution through `workspace_shell("connection query ... --json")`
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
Install from PyPI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 -m pip install marcopolo-sdk
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Install for local development:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3 -m pip install -e ".[dev]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Build Release Artifacts
|
|
66
|
+
|
|
67
|
+
Build and validate the PyPI release artifacts locally:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 -m pip install -e ".[dev]"
|
|
71
|
+
python3 -m build
|
|
72
|
+
python3 -m twine check dist/*
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The built source distribution and wheel are written under `dist/`.
|
|
76
|
+
|
|
77
|
+
## Publish to PyPI
|
|
78
|
+
|
|
79
|
+
This repository is set up for GitHub Actions based PyPI trusted publishing.
|
|
80
|
+
|
|
81
|
+
High-level flow:
|
|
82
|
+
|
|
83
|
+
1. Configure the one-time trusted publisher on PyPI for this repository.
|
|
84
|
+
2. Push a version tag such as `v0.1.1`.
|
|
85
|
+
3. Create a GitHub release from that tag.
|
|
86
|
+
4. The `publish-pypi.yml` workflow builds the package and uploads it to PyPI.
|
|
87
|
+
|
|
88
|
+
See [`PYPI_PUBLISHING.md`](PYPI_PUBLISHING.md) for the concrete setup steps.
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
`MarcoPolo` does not read `.env` files or process environment variables.
|
|
93
|
+
Your application owns configuration loading and must pass explicit settings into
|
|
94
|
+
the constructor.
|
|
95
|
+
|
|
96
|
+
## Public API
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import os
|
|
100
|
+
|
|
101
|
+
from marcopolo import MarcoPolo
|
|
102
|
+
|
|
103
|
+
marcopolo = MarcoPolo(
|
|
104
|
+
api_token=os.environ["MARCOPOLO_API_TOKEN"],
|
|
105
|
+
server_url=os.environ["MARCOPOLO_MCP_SERVER_URL"],
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `list_connections()`
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
async def list_connections(
|
|
113
|
+
*,
|
|
114
|
+
context: str,
|
|
115
|
+
timeout: int | None = None,
|
|
116
|
+
) -> ConnectionListResult
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Behavior:
|
|
120
|
+
|
|
121
|
+
- executes `workspace_shell("connection list --json")`
|
|
122
|
+
- parses the returned shell payload and nested JSON `stdout`
|
|
123
|
+
- returns normalized connection metadata including capabilities
|
|
124
|
+
|
|
125
|
+
### `execute()`
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
async def execute(
|
|
129
|
+
connection_name: str,
|
|
130
|
+
payload: dict | list | str,
|
|
131
|
+
*,
|
|
132
|
+
query_name: str,
|
|
133
|
+
context: str,
|
|
134
|
+
payload_format: Literal["json", "sql", "text"] | None = None,
|
|
135
|
+
params: dict | None = None,
|
|
136
|
+
timeout: int | None = None,
|
|
137
|
+
) -> ExecutionResult
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Behavior:
|
|
141
|
+
|
|
142
|
+
- writes a durable remote query file under
|
|
143
|
+
`connections/<connection_name>/queries/`
|
|
144
|
+
- executes it with `connection query <connection_name> --file <query_file> --json`
|
|
145
|
+
- always requests the full result set by passing `--sample-rows -1` internally
|
|
146
|
+
- normalizes the result into `ExecutionResult`
|
|
147
|
+
|
|
148
|
+
### `execute_query_file()`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
async def execute_query_file(
|
|
152
|
+
connection_name: str,
|
|
153
|
+
query_file: str,
|
|
154
|
+
*,
|
|
155
|
+
context: str,
|
|
156
|
+
params: dict | None = None,
|
|
157
|
+
timeout: int | None = None,
|
|
158
|
+
) -> ExecutionResult
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use this when the query file already exists in the MarcoPolo workspace and you
|
|
162
|
+
only want execution.
|
|
163
|
+
|
|
164
|
+
## Payload Rules
|
|
165
|
+
|
|
166
|
+
The client is syntax-agnostic. It does not read connector `SYNTAX.md` files or
|
|
167
|
+
infer connector semantics. The caller is responsible for sending a payload that
|
|
168
|
+
is valid for the target connection.
|
|
169
|
+
|
|
170
|
+
Serialization rules:
|
|
171
|
+
|
|
172
|
+
- `dict` and `list` payloads are serialized as pretty JSON and written as
|
|
173
|
+
`.json`
|
|
174
|
+
- raw `str` payloads require explicit `payload_format`
|
|
175
|
+
- supported raw string formats are `json`, `sql`, and `text`
|
|
176
|
+
- `query_name` is mandatory and is sanitized into a readable underscore-based
|
|
177
|
+
filename
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
|
|
181
|
+
- `query_name="Top 5 Accounts By Revenue"` becomes
|
|
182
|
+
`top_5_accounts_by_revenue.json`
|
|
183
|
+
- `query_name="Loki Errors Last 24h"` becomes
|
|
184
|
+
`loki_errors_last_24h.json`
|
|
185
|
+
|
|
186
|
+
## Usage
|
|
187
|
+
|
|
188
|
+
The client is async-only by design. A simple application entrypoint can use
|
|
189
|
+
`asyncio.run(...)`.
|
|
190
|
+
|
|
191
|
+
### Jira read
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import asyncio
|
|
195
|
+
import os
|
|
196
|
+
|
|
197
|
+
from marcopolo import MarcoPolo
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def main() -> None:
|
|
201
|
+
marcopolo = MarcoPolo(
|
|
202
|
+
api_token=os.environ["MARCOPOLO_API_TOKEN"],
|
|
203
|
+
server_url=os.environ["MARCOPOLO_MCP_SERVER_URL"],
|
|
204
|
+
)
|
|
205
|
+
connections = await marcopolo.list_connections(
|
|
206
|
+
context="List available governed connections before choosing one.",
|
|
207
|
+
)
|
|
208
|
+
print(connections.count)
|
|
209
|
+
print(connections.connections[:2])
|
|
210
|
+
|
|
211
|
+
result = await marcopolo.execute(
|
|
212
|
+
"jira-jql-20260710-1527",
|
|
213
|
+
{
|
|
214
|
+
"jql": (
|
|
215
|
+
"assignee = currentUser() "
|
|
216
|
+
"AND statusCategory != Done ORDER BY updated DESC"
|
|
217
|
+
),
|
|
218
|
+
"fields": [
|
|
219
|
+
"issuekey",
|
|
220
|
+
"summary",
|
|
221
|
+
"status",
|
|
222
|
+
"priority",
|
|
223
|
+
"project",
|
|
224
|
+
"assignee",
|
|
225
|
+
"created",
|
|
226
|
+
"updated",
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
query_name="open_tickets_current_user",
|
|
230
|
+
context="Load current open Jira tickets for the current Jira user.",
|
|
231
|
+
)
|
|
232
|
+
print(result.row_count)
|
|
233
|
+
print(result.rows[:2])
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
asyncio.run(main())
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Google Drive read
|
|
240
|
+
|
|
241
|
+
Validated live against `google-drive-20260710-1517` and covered by
|
|
242
|
+
`tests/test_integration_reads.py::test_execute_google_drive_sheet_read`.
|
|
243
|
+
For the current connection scope, the working form is the plain display name
|
|
244
|
+
`sales-by-quarter`; a folder-qualified path such as
|
|
245
|
+
`some-folder/sales-by-quarter` does not resolve.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
result = await marcopolo.execute(
|
|
249
|
+
"google-drive-20260710-1517",
|
|
250
|
+
{
|
|
251
|
+
"file": "sales-by-quarter",
|
|
252
|
+
"sheet": "0",
|
|
253
|
+
},
|
|
254
|
+
query_name="sales_by_quarter_sheet0",
|
|
255
|
+
context="Read the sales-by-quarter spreadsheet from Google Drive.",
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Loki read
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
result = await marcopolo.execute(
|
|
263
|
+
"grafana-loki-20260519-2152",
|
|
264
|
+
{
|
|
265
|
+
"operation": "query_range",
|
|
266
|
+
"query": '{job=~".+"} |~ "(?i)error"',
|
|
267
|
+
"start": "now-24h",
|
|
268
|
+
"end": "now",
|
|
269
|
+
"limit": 200,
|
|
270
|
+
"direction": "backward",
|
|
271
|
+
},
|
|
272
|
+
query_name="errors_last_24h",
|
|
273
|
+
context="Read recent error logs from Loki.",
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Salesforce update
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
result = await marcopolo.execute(
|
|
281
|
+
"salesforce-demo-3841cee8-20260709-2149",
|
|
282
|
+
{
|
|
283
|
+
"endpoint": "/services/data/v47.0/sobjects/Account/001gK00000DFg5tQAD",
|
|
284
|
+
"method": "PATCH",
|
|
285
|
+
"body": {
|
|
286
|
+
"Description": "Customer since 2024-01-24. Tier: enterprise",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
query_name="update_existing_account_description",
|
|
290
|
+
context="Apply a non-destructive Salesforce account update.",
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Salesforce insert
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
result = await marcopolo.execute(
|
|
298
|
+
"salesforce-demo-3841cee8-20260709-2149",
|
|
299
|
+
{
|
|
300
|
+
"endpoint": "/services/data/v47.0/sobjects/Opportunity",
|
|
301
|
+
"method": "POST",
|
|
302
|
+
"body": {
|
|
303
|
+
"Name": "MarcoPolo Client Example Opportunity",
|
|
304
|
+
"StageName": "Prospecting",
|
|
305
|
+
"CloseDate": "2026-07-31",
|
|
306
|
+
"Description": "Created from the MarcoPolo client README example",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
query_name="create_example_opportunity",
|
|
310
|
+
context="Create a Salesforce opportunity through the MarcoPolo client.",
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Execute an existing remote query file
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
result = await marcopolo.execute_query_file(
|
|
318
|
+
"google-drive-20260710-1517",
|
|
319
|
+
"connections/google-drive-20260710-1517/queries/sales_by_quarter_sheet0.json",
|
|
320
|
+
context="Execute a pre-authored Google Drive query file.",
|
|
321
|
+
)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Result Model
|
|
325
|
+
|
|
326
|
+
`execute()` and `execute_query_file()` return `ExecutionResult`:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
ExecutionResult(
|
|
330
|
+
connection_name: str,
|
|
331
|
+
query_file: str,
|
|
332
|
+
rows: list[dict[str, Any]],
|
|
333
|
+
row_count: int,
|
|
334
|
+
run_id: str | None,
|
|
335
|
+
raw_payload: dict[str, Any],
|
|
336
|
+
raw_command_result: dict[str, Any],
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Notes:
|
|
341
|
+
|
|
342
|
+
- `rows` is extracted from `data` or `preview`
|
|
343
|
+
- `row_count` uses the command payload value when present, otherwise `len(rows)`
|
|
344
|
+
- write operations may still return useful rows, such as Salesforce create
|
|
345
|
+
responses with inserted record IDs
|
|
346
|
+
- `raw_payload` and `raw_command_result` are preserved for debugging
|
|
347
|
+
|
|
348
|
+
`list_connections()` returns `ConnectionListResult`:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
ConnectionListResult(
|
|
352
|
+
connections: list[ConnectionSummary],
|
|
353
|
+
count: int,
|
|
354
|
+
message: str | None,
|
|
355
|
+
next_actions: list[str],
|
|
356
|
+
raw_payload: dict[str, Any],
|
|
357
|
+
raw_command_result: dict[str, Any],
|
|
358
|
+
)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Where each `ConnectionSummary` includes:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
ConnectionSummary(
|
|
365
|
+
name: str,
|
|
366
|
+
connection_type: str,
|
|
367
|
+
capabilities: list[str],
|
|
368
|
+
display_name: str | None,
|
|
369
|
+
workspace_path: str | None,
|
|
370
|
+
)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Observed Result Shapes
|
|
374
|
+
|
|
375
|
+
The exact row schema is connector-specific. The client does not reshape rows
|
|
376
|
+
beyond extracting them from the command response. These are representative
|
|
377
|
+
live-observed examples from the validated connectors.
|
|
378
|
+
|
|
379
|
+
### Jira rows
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
[
|
|
383
|
+
{
|
|
384
|
+
"key": "IMMERSA-455",
|
|
385
|
+
"summary": "Example issue title",
|
|
386
|
+
"created": "2026-05-11T18:28:05.844+0000",
|
|
387
|
+
"project_key": "IMMERSA",
|
|
388
|
+
"project_name": "Immersa",
|
|
389
|
+
"assignee": "{\"displayName\": \"Example User\", ...}",
|
|
390
|
+
"priority_name": "Medium",
|
|
391
|
+
"updated": "2026-05-11T18:28:07.126+0000",
|
|
392
|
+
"status_name": "To Do",
|
|
393
|
+
},
|
|
394
|
+
...,
|
|
395
|
+
]
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Google Drive rows
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
[
|
|
402
|
+
{
|
|
403
|
+
"customer_id": 1,
|
|
404
|
+
"quarter_end_dt": "03/31/2025",
|
|
405
|
+
"billing_amount_usd": 100,
|
|
406
|
+
},
|
|
407
|
+
...,
|
|
408
|
+
]
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Loki rows
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
[
|
|
415
|
+
{
|
|
416
|
+
"timestamp": "2026-07-10T17:42:31.123456Z",
|
|
417
|
+
"line": "ERROR request failed for job=api",
|
|
418
|
+
"labels": "{\"job\": \"duploservices-prod01/mproxy\", ...}",
|
|
419
|
+
},
|
|
420
|
+
...,
|
|
421
|
+
]
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Salesforce insert rows
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
[
|
|
428
|
+
{
|
|
429
|
+
"id": "006gK00000KlbtRQAR",
|
|
430
|
+
"success": True,
|
|
431
|
+
"errors": "[]",
|
|
432
|
+
},
|
|
433
|
+
...,
|
|
434
|
+
]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Salesforce update and delete rows
|
|
438
|
+
|
|
439
|
+
For successful update and delete operations, the command payload may report
|
|
440
|
+
`row_count = 1` while `rows` is empty because the connector returned no tabular
|
|
441
|
+
data:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
result.row_count == 1
|
|
445
|
+
result.rows == []
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Raw command payload
|
|
449
|
+
|
|
450
|
+
`ExecutionResult.raw_payload` is the parsed JSON body returned by
|
|
451
|
+
`connection query --json`. Representative shape:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
{
|
|
455
|
+
"success": True,
|
|
456
|
+
"data": "[{\"id\":\"006gK00000KlbtRQAR\",\"success\":true,\"errors\":\"[]\"}]",
|
|
457
|
+
"preview": "[{\"id\":\"006gK00000KlbtRQAR\",\"success\":true,\"errors\":\"[]\"}]",
|
|
458
|
+
"row_count": 1,
|
|
459
|
+
"run_id": "run_123",
|
|
460
|
+
"query_file": (
|
|
461
|
+
"connections/salesforce-demo-3841cee8-20260709-2149/"
|
|
462
|
+
"queries/create_example_opportunity.json"
|
|
463
|
+
),
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Development
|
|
468
|
+
|
|
469
|
+
Run lint:
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
ruff check .
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Run tests:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
pytest -q
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The test suite is live-only. Before running it, load these variables into your
|
|
482
|
+
shell by whatever mechanism your environment uses:
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
export MARCOPOLO_API_TOKEN=...
|
|
486
|
+
export MARCOPOLO_MCP_SERVER_URL=...
|
|
487
|
+
pytest -q -s
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Current Limitations
|
|
491
|
+
|
|
492
|
+
- The client does not inspect connection `SYNTAX.md` files. Payload formation
|
|
493
|
+
is fully caller-owned.
|
|
494
|
+
- The first version exposes execution only. Higher-level `query()` helpers and
|
|
495
|
+
connector-specific convenience wrappers are deferred.
|
|
496
|
+
- Jira create/update is not documented by the currently validated Jira
|
|
497
|
+
connection surface, so the examples stay read-only.
|
|
498
|
+
- The Google Drive spec example using `test1` / `test` is environment-dependent
|
|
499
|
+
and not currently available in the active validated connection scope.
|
|
500
|
+
- For the validated Google Drive spreadsheet example, use the authorized file
|
|
501
|
+
display name `sales-by-quarter` or a file ID/URL. Folder-qualified display
|
|
502
|
+
paths such as `some-folder/sales-by-quarter` are not currently resolved by
|
|
503
|
+
the active connection.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# PyPI Publishing
|
|
2
|
+
|
|
3
|
+
This repository publishes the public package `marcopolo-sdk` to PyPI using
|
|
4
|
+
GitHub Actions trusted publishing.
|
|
5
|
+
|
|
6
|
+
## Recommended model
|
|
7
|
+
|
|
8
|
+
- Source repository: `immersa-co/marcopolo-python-sdk`
|
|
9
|
+
- Distribution name on PyPI: `marcopolo-sdk`
|
|
10
|
+
- Import package in user code: `marcopolo`
|
|
11
|
+
- Release trigger: publish a GitHub release from a version tag such as
|
|
12
|
+
`v0.1.1`
|
|
13
|
+
|
|
14
|
+
## One-time PyPI setup
|
|
15
|
+
|
|
16
|
+
1. Create or log in to the PyPI account that will administer the project.
|
|
17
|
+
2. Create a pending trusted publisher for this repository on PyPI.
|
|
18
|
+
|
|
19
|
+
Use these values:
|
|
20
|
+
|
|
21
|
+
- PyPI project name: `marcopolo-sdk`
|
|
22
|
+
- Owner: `immersa-co`
|
|
23
|
+
- Repository name: `marcopolo-python-sdk`
|
|
24
|
+
- Workflow filename: `publish-pypi.yml`
|
|
25
|
+
- Environment name: `pypi`
|
|
26
|
+
|
|
27
|
+
If the project does not exist yet on PyPI, create it through the trusted
|
|
28
|
+
publisher flow rather than performing a manual first upload.
|
|
29
|
+
|
|
30
|
+
## GitHub workflow files
|
|
31
|
+
|
|
32
|
+
- `.github/workflows/package-check.yml`
|
|
33
|
+
Runs lint, builds distributions, and validates metadata on push and PR.
|
|
34
|
+
- `.github/workflows/publish-pypi.yml`
|
|
35
|
+
Builds and publishes to PyPI using GitHub OIDC trusted publishing.
|
|
36
|
+
|
|
37
|
+
## Release process
|
|
38
|
+
|
|
39
|
+
1. Update the version in `pyproject.toml` and `src/marcopolo/_version.py`.
|
|
40
|
+
2. Commit the version change and push it to `main`.
|
|
41
|
+
3. Create and push a version tag:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git tag -a v0.1.1 -m "v0.1.1"
|
|
45
|
+
git push origin v0.1.1
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
4. Create a GitHub release for that tag and mark it published.
|
|
49
|
+
5. Confirm the `Publish to PyPI` workflow succeeds.
|
|
50
|
+
6. Verify the release on PyPI:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python3 -m pip install --upgrade marcopolo-sdk
|
|
54
|
+
python3 - <<'PY'
|
|
55
|
+
import marcopolo
|
|
56
|
+
print(marcopolo.__version__)
|
|
57
|
+
PY
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Local validation before release
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python3 -m pip install -e ".[dev]"
|
|
64
|
+
python3 -m build
|
|
65
|
+
python3 -m twine check dist/*
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
|
|
70
|
+
- PyPI publishes the distribution name `marcopolo-sdk`, while application code
|
|
71
|
+
imports `marcopolo`.
|
|
72
|
+
- The publish workflow does not store a long-lived PyPI API token in GitHub.
|
|
73
|
+
- The `pypi` environment can be protected with required reviewers if you want
|
|
74
|
+
human approval before a release is published.
|