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.
@@ -0,0 +1,3 @@
1
+ # Copy values into .env for local development.
2
+ MARCOPOLO_API_TOKEN=
3
+ MARCOPOLO_MCP_SERVER_URL=https://mcp.marcopolo.dev
@@ -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,14 @@
1
+ .env*
2
+ !.env.example
3
+ ADR.md
4
+ MANUAL_VALIDATION.md
5
+ .MANUAL_VALIDATION.md.swp
6
+ .beads/
7
+ __pycache__/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .venv*/
11
+ build/
12
+ dist/
13
+ *.pyc
14
+ *.swp
@@ -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.