spooling 0.2.2__tar.gz → 0.2.4__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.
- {spooling-0.2.2/spooling.egg-info → spooling-0.2.4}/PKG-INFO +1 -1
- {spooling-0.2.2 → spooling-0.2.4}/pyproject.toml +1 -1
- {spooling-0.2.2 → spooling-0.2.4}/spooling/cloud.py +13 -5
- {spooling-0.2.2 → spooling-0.2.4}/spooling/tunnel.py +91 -19
- {spooling-0.2.2 → spooling-0.2.4/spooling.egg-info}/PKG-INFO +1 -1
- {spooling-0.2.2 → spooling-0.2.4}/LICENSE +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/README.md +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/setup.cfg +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/agent.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/classifiers.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/cli.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/config.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/base.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/bigquery.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/clickhouse.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/factory.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/mongodb.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/mysql.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/postgresql.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/rest/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/rest/base.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/rest/shopify_connectors.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/snowflake.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/connectors/types.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/db.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/embeddings.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/evals.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/experiments.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/ingest.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/mcp_server.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/parser.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/pricing.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/antigravity.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/base.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/codex.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/copilot.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/cursor.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/gemini.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/github.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/gitlab.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/kiro.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/opencode.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/session_file.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/providers/windsurf.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/redact.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/remote_otel.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/schema.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/sdk.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/search.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/server.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/stats.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/subscription_pricing.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/tracing.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/tunnel_auth.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling/watcher.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling.egg-info/SOURCES.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling.egg-info/requires.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/spooling.egg-info/top_level.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/tests/test_mcp_auth_middleware.py +0 -0
- {spooling-0.2.2 → spooling-0.2.4}/tests/test_tunnel_auth.py +0 -0
|
@@ -221,6 +221,14 @@ def _collect_traces(session_ids: list[str]) -> list[dict]:
|
|
|
221
221
|
"""
|
|
222
222
|
if not session_ids:
|
|
223
223
|
return []
|
|
224
|
+
|
|
225
|
+
def _ts(v) -> str | None:
|
|
226
|
+
"""Return an ISO-8601 string regardless of whether v is a datetime or str."""
|
|
227
|
+
if v is None:
|
|
228
|
+
return None
|
|
229
|
+
if isinstance(v, str):
|
|
230
|
+
return v
|
|
231
|
+
return v.isoformat()
|
|
224
232
|
conn = get_connection()
|
|
225
233
|
try:
|
|
226
234
|
placeholders = ", ".join(["%s"] * len(session_ids))
|
|
@@ -262,8 +270,8 @@ def _collect_traces(session_ids: list[str]) -> list[dict]:
|
|
|
262
270
|
"kind": s["kind"],
|
|
263
271
|
"name": _clean_str(s["name"], _MAX_SPAN_NAME),
|
|
264
272
|
"status": s["status"] or "ok",
|
|
265
|
-
"started_at": s["started_at"]
|
|
266
|
-
"ended_at": s["ended_at"]
|
|
273
|
+
"started_at": _ts(s["started_at"]),
|
|
274
|
+
"ended_at": _ts(s["ended_at"]),
|
|
267
275
|
"duration_ms": int(s["duration_ms"] or 0),
|
|
268
276
|
"depth": int(s["depth"] or 0),
|
|
269
277
|
"sequence": int(s["sequence"] or 0),
|
|
@@ -285,7 +293,7 @@ def _collect_traces(session_ids: list[str]) -> list[dict]:
|
|
|
285
293
|
"events": [
|
|
286
294
|
{
|
|
287
295
|
"name": e["name"],
|
|
288
|
-
"timestamp": e["timestamp"]
|
|
296
|
+
"timestamp": _ts(e["timestamp"]),
|
|
289
297
|
"attrs": e["attrs"] or {},
|
|
290
298
|
}
|
|
291
299
|
for e in events
|
|
@@ -298,8 +306,8 @@ def _collect_traces(session_ids: list[str]) -> list[dict]:
|
|
|
298
306
|
"provider_id": t["provider_id"],
|
|
299
307
|
"project": t["project"],
|
|
300
308
|
"title": _clean_str(t["title"], _MAX_TITLE),
|
|
301
|
-
"started_at": t["started_at"]
|
|
302
|
-
"ended_at": t["ended_at"]
|
|
309
|
+
"started_at": _ts(t["started_at"]),
|
|
310
|
+
"ended_at": _ts(t["ended_at"]),
|
|
303
311
|
"duration_ms": int(t["duration_ms"] or 0),
|
|
304
312
|
"span_count": int(t["span_count"] or 0),
|
|
305
313
|
"agent_count": int(t["agent_count"] or 0),
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"""Cloudflare tunnel support for exposing local MCP servers."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
3
5
|
import re
|
|
6
|
+
import stat
|
|
4
7
|
import subprocess
|
|
5
8
|
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
6
11
|
from typing import Optional, Tuple
|
|
7
12
|
|
|
8
13
|
from rich.console import Console
|
|
@@ -12,19 +17,85 @@ console = Console()
|
|
|
12
17
|
# Pattern to match Cloudflare quick tunnel URLs
|
|
13
18
|
TUNNEL_URL_PATTERN = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
|
14
19
|
|
|
20
|
+
# Where we cache the auto-downloaded binary
|
|
21
|
+
_BIN_DIR = Path.home() / ".spooling" / "bin"
|
|
22
|
+
_CLOUDFLARED_BIN = _BIN_DIR / ("cloudflared.exe" if platform.system() == "Windows" else "cloudflared")
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
# Download URL templates per platform/arch
|
|
25
|
+
_DOWNLOAD_URLS: dict[tuple[str, str], str] = {
|
|
26
|
+
("Darwin", "arm64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64",
|
|
27
|
+
("Darwin", "x86_64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64",
|
|
28
|
+
("Linux", "aarch64"):"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64",
|
|
29
|
+
("Linux", "x86_64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64",
|
|
30
|
+
("Windows", "AMD64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cloudflared_cmd() -> Optional[str]:
|
|
35
|
+
"""Return the path to a usable cloudflared binary, or None."""
|
|
36
|
+
# 1. Already on PATH
|
|
18
37
|
try:
|
|
19
|
-
result = subprocess.run(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
text=True,
|
|
23
|
-
timeout=5,
|
|
24
|
-
)
|
|
25
|
-
return result.returncode == 0
|
|
38
|
+
result = subprocess.run(["cloudflared", "version"], capture_output=True, timeout=5)
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
return "cloudflared"
|
|
26
41
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
27
|
-
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# 2. Previously auto-downloaded
|
|
45
|
+
if _CLOUDFLARED_BIN.exists():
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run([str(_CLOUDFLARED_BIN), "version"], capture_output=True, timeout=5)
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
return str(_CLOUDFLARED_BIN)
|
|
50
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _auto_install_cloudflared() -> Optional[str]:
|
|
57
|
+
"""Download the cloudflared binary for this platform into ~/.spooling/bin/.
|
|
58
|
+
|
|
59
|
+
Returns the path to the binary on success, None on failure.
|
|
60
|
+
"""
|
|
61
|
+
system = platform.system()
|
|
62
|
+
machine = platform.machine()
|
|
63
|
+
url = _DOWNLOAD_URLS.get((system, machine))
|
|
64
|
+
|
|
65
|
+
if not url:
|
|
66
|
+
console.print(f"[red]No cloudflared binary available for {system}/{machine}.[/red]")
|
|
67
|
+
console.print("Install manually: https://developers.cloudflare.com/cloudflared/install/")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
_BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
console.print(f"[bold]Downloading cloudflared for {system}/{machine}...[/bold]")
|
|
73
|
+
try:
|
|
74
|
+
urllib.request.urlretrieve(url, _CLOUDFLARED_BIN)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
console.print(f"[red]Download failed:[/red] {e}")
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Make executable on Unix
|
|
80
|
+
if system != "Windows":
|
|
81
|
+
_CLOUDFLARED_BIN.chmod(_CLOUDFLARED_BIN.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
82
|
+
|
|
83
|
+
# Verify
|
|
84
|
+
try:
|
|
85
|
+
result = subprocess.run([str(_CLOUDFLARED_BIN), "version"], capture_output=True, timeout=5)
|
|
86
|
+
if result.returncode == 0:
|
|
87
|
+
console.print(f"[green]cloudflared installed to {_CLOUDFLARED_BIN}[/green]")
|
|
88
|
+
return str(_CLOUDFLARED_BIN)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
console.print("[red]Downloaded binary did not run correctly.[/red]")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_cloudflared() -> bool:
|
|
97
|
+
"""Check if cloudflared is available (on PATH or auto-downloaded)."""
|
|
98
|
+
return _cloudflared_cmd() is not None
|
|
28
99
|
|
|
29
100
|
|
|
30
101
|
def _validate_port(port: int) -> bool:
|
|
@@ -49,15 +120,13 @@ def start_tunnel(
|
|
|
49
120
|
console.print(f"[red]Invalid port: {port}. Must be 1-65535.[/red]")
|
|
50
121
|
return None
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.print(" Linux: [bold]curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared[/bold]")
|
|
57
|
-
console.print(" Windows: [bold]winget install cloudflare.cloudflared[/bold]")
|
|
123
|
+
cmd_bin = _cloudflared_cmd()
|
|
124
|
+
if not cmd_bin:
|
|
125
|
+
cmd_bin = _auto_install_cloudflared()
|
|
126
|
+
if not cmd_bin:
|
|
58
127
|
return None
|
|
59
128
|
|
|
60
|
-
cmd = [
|
|
129
|
+
cmd = [cmd_bin, "tunnel", "--no-autoupdate", "--url", f"http://localhost:{port}"]
|
|
61
130
|
|
|
62
131
|
console.print(f"[bold]Starting Cloudflare tunnel to localhost:{port}...[/bold]")
|
|
63
132
|
|
|
@@ -142,10 +211,13 @@ def start_tunnel_background(
|
|
|
142
211
|
if not _validate_port(port):
|
|
143
212
|
return None, None
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
cmd_bin = _cloudflared_cmd()
|
|
215
|
+
if not cmd_bin:
|
|
216
|
+
cmd_bin = _auto_install_cloudflared()
|
|
217
|
+
if not cmd_bin:
|
|
146
218
|
return None, None
|
|
147
219
|
|
|
148
|
-
cmd = [
|
|
220
|
+
cmd = [cmd_bin, "tunnel", "--no-autoupdate", "--url", f"http://localhost:{port}"]
|
|
149
221
|
|
|
150
222
|
process = subprocess.Popen(
|
|
151
223
|
cmd,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|