quraite 0.0.1__py3-none-any.whl → 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.
- quraite/__init__.py +3 -3
- quraite/adapters/__init__.py +134 -134
- quraite/adapters/agno_adapter.py +159 -159
- quraite/adapters/base.py +123 -123
- quraite/adapters/bedrock_agents_adapter.py +343 -343
- quraite/adapters/flowise_adapter.py +275 -275
- quraite/adapters/google_adk_adapter.py +209 -209
- quraite/adapters/http_adapter.py +239 -239
- quraite/adapters/langflow_adapter.py +192 -192
- quraite/adapters/langgraph_adapter.py +304 -304
- quraite/adapters/langgraph_server_adapter.py +252 -252
- quraite/adapters/n8n_adapter.py +220 -220
- quraite/adapters/openai_agents_adapter.py +269 -269
- quraite/adapters/pydantic_ai_adapter.py +312 -312
- quraite/adapters/smolagents_adapter.py +152 -152
- quraite/logger.py +61 -62
- quraite/schema/message.py +91 -54
- quraite/schema/response.py +16 -16
- quraite/serve/__init__.py +1 -1
- quraite/serve/cloudflared.py +210 -210
- quraite/serve/local_agent.py +360 -360
- quraite/tracing/__init__.py +24 -24
- quraite/tracing/constants.py +16 -16
- quraite/tracing/span_exporter.py +115 -115
- quraite/tracing/span_processor.py +49 -49
- quraite/tracing/tool_extractors.py +290 -290
- quraite/tracing/trace.py +564 -494
- quraite/tracing/types.py +179 -179
- quraite/tracing/utils.py +170 -170
- quraite/utils/json_utils.py +269 -269
- {quraite-0.0.1.dist-info → quraite-0.1.0.dist-info}/METADATA +9 -9
- quraite-0.1.0.dist-info/RECORD +35 -0
- {quraite-0.0.1.dist-info → quraite-0.1.0.dist-info}/WHEEL +1 -1
- quraite/traces/traces_adk_openinference.json +0 -379
- quraite/traces/traces_agno_multi_agent.json +0 -669
- quraite/traces/traces_agno_openinference.json +0 -321
- quraite/traces/traces_crewai_openinference.json +0 -155
- quraite/traces/traces_langgraph_openinference.json +0 -349
- quraite/traces/traces_langgraph_openinference_multi_agent.json +0 -2705
- quraite/traces/traces_langgraph_traceloop.json +0 -510
- quraite/traces/traces_openai_agents_multi_agent_1.json +0 -402
- quraite/traces/traces_openai_agents_openinference.json +0 -341
- quraite/traces/traces_pydantic_openinference.json +0 -286
- quraite/traces/traces_pydantic_openinference_multi_agent_1.json +0 -399
- quraite/traces/traces_pydantic_openinference_multi_agent_2.json +0 -398
- quraite/traces/traces_smol_agents_openinference.json +0 -397
- quraite/traces/traces_smol_agents_tool_calling_openinference.json +0 -704
- quraite-0.0.1.dist-info/RECORD +0 -49
quraite/serve/cloudflared.py
CHANGED
|
@@ -1,210 +1,210 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import platform
|
|
3
|
-
import re
|
|
4
|
-
import shutil
|
|
5
|
-
import stat
|
|
6
|
-
import subprocess
|
|
7
|
-
import threading
|
|
8
|
-
import time
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from urllib.request import urlopen
|
|
11
|
-
|
|
12
|
-
from quraite.logger import get_logger
|
|
13
|
-
|
|
14
|
-
# Cloudflare Tunnel download URLs
|
|
15
|
-
CLOUDFLARED_RELEASES_URL = (
|
|
16
|
-
"https://github.com/cloudflare/cloudflared/releases/latest/download"
|
|
17
|
-
)
|
|
18
|
-
PLATFORMS = {
|
|
19
|
-
"darwin_x86_64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-darwin-amd64",
|
|
20
|
-
"darwin_arm64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-darwin-arm64",
|
|
21
|
-
"windows_x86_64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-windows-amd64.exe",
|
|
22
|
-
"linux_x86_64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-linux-amd64",
|
|
23
|
-
"linux_arm64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-linux-arm64",
|
|
24
|
-
"linux_arm": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-linux-arm",
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
logger = get_logger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class CloudflaredError(Exception):
|
|
31
|
-
"""Base exception for cloudflared operations."""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class CloudflaredTunnel:
|
|
35
|
-
"""Represents a Cloudflare Tunnel connection."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, public_url: str, process: subprocess.Popen):
|
|
38
|
-
self.public_url = public_url
|
|
39
|
-
self._process = process
|
|
40
|
-
|
|
41
|
-
def disconnect(self):
|
|
42
|
-
"""Disconnect the tunnel."""
|
|
43
|
-
if self._process and self._process.poll() is None:
|
|
44
|
-
self._process.terminate()
|
|
45
|
-
try:
|
|
46
|
-
self._process.wait(timeout=5)
|
|
47
|
-
except subprocess.TimeoutExpired:
|
|
48
|
-
self._process.kill()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def get_system() -> str:
|
|
52
|
-
"""Get the system platform identifier."""
|
|
53
|
-
system = platform.system().lower()
|
|
54
|
-
machine = platform.machine().lower()
|
|
55
|
-
|
|
56
|
-
if system == "darwin":
|
|
57
|
-
if machine in ("arm64", "aarch64"):
|
|
58
|
-
return "darwin_arm64"
|
|
59
|
-
return "darwin_x86_64"
|
|
60
|
-
elif system == "windows":
|
|
61
|
-
return "windows_x86_64"
|
|
62
|
-
elif system == "linux":
|
|
63
|
-
if machine in ("arm64", "aarch64"):
|
|
64
|
-
return "linux_arm64"
|
|
65
|
-
elif machine.startswith("arm"):
|
|
66
|
-
return "linux_arm"
|
|
67
|
-
return "linux_x86_64"
|
|
68
|
-
else:
|
|
69
|
-
raise CloudflaredError(f"Unsupported platform: {system} {machine}")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def get_cloudflared_path() -> Path:
|
|
73
|
-
"""Get the path where cloudflared binary should be stored."""
|
|
74
|
-
user_home = Path.home()
|
|
75
|
-
system = platform.system().lower()
|
|
76
|
-
|
|
77
|
-
if system == "darwin":
|
|
78
|
-
config_dir = user_home / "Library" / "Application Support" / "cloudflared"
|
|
79
|
-
elif system == "windows":
|
|
80
|
-
config_dir = user_home / "AppData" / "Local" / "cloudflared"
|
|
81
|
-
else:
|
|
82
|
-
config_dir = (
|
|
83
|
-
Path(os.environ.get("XDG_CONFIG_HOME", user_home / ".config"))
|
|
84
|
-
/ "cloudflared"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
-
|
|
89
|
-
if system == "windows":
|
|
90
|
-
return config_dir / "cloudflared.exe"
|
|
91
|
-
return config_dir / "cloudflared"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def download_cloudflared(force: bool = False) -> Path:
|
|
95
|
-
"""
|
|
96
|
-
Download cloudflared binary for the current platform.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
force: If True, re-download even if binary exists
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Path to the cloudflared binary
|
|
103
|
-
"""
|
|
104
|
-
# Check if cloudflared is already in PATH
|
|
105
|
-
cloudflared_cmd = (
|
|
106
|
-
"cloudflared.exe" if platform.system() == "windows" else "cloudflared"
|
|
107
|
-
)
|
|
108
|
-
if shutil.which(cloudflared_cmd) and not force:
|
|
109
|
-
return Path(shutil.which(cloudflared_cmd))
|
|
110
|
-
|
|
111
|
-
cloudflared_path = get_cloudflared_path()
|
|
112
|
-
system_key = get_system()
|
|
113
|
-
|
|
114
|
-
if cloudflared_path.exists() and not force:
|
|
115
|
-
# Check if binary is executable
|
|
116
|
-
if platform.system() != "windows":
|
|
117
|
-
st = os.stat(cloudflared_path)
|
|
118
|
-
os.chmod(cloudflared_path, st.st_mode | stat.S_IEXEC)
|
|
119
|
-
return cloudflared_path
|
|
120
|
-
|
|
121
|
-
if system_key not in PLATFORMS:
|
|
122
|
-
raise CloudflaredError(f"Unsupported platform: {system_key}")
|
|
123
|
-
|
|
124
|
-
download_url = PLATFORMS[system_key]
|
|
125
|
-
logger.info("Downloading cloudflared from %s...", download_url)
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
with urlopen(download_url, timeout=30) as response:
|
|
129
|
-
with open(cloudflared_path, "wb") as f:
|
|
130
|
-
f.write(response.read())
|
|
131
|
-
|
|
132
|
-
# Make executable on Unix systems
|
|
133
|
-
if platform.system() != "windows":
|
|
134
|
-
st = os.stat(cloudflared_path)
|
|
135
|
-
os.chmod(cloudflared_path, st.st_mode | stat.S_IEXEC)
|
|
136
|
-
|
|
137
|
-
logger.info("Downloaded cloudflared to %s", cloudflared_path)
|
|
138
|
-
return cloudflared_path
|
|
139
|
-
|
|
140
|
-
except Exception as e:
|
|
141
|
-
raise CloudflaredError(f"Failed to download cloudflared: {e}") from e
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def connect(port: int, host: str = "localhost") -> CloudflaredTunnel:
|
|
145
|
-
"""
|
|
146
|
-
Create a Cloudflare Tunnel connection to the specified port.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
port: Local port to tunnel
|
|
150
|
-
host: Local host (default: localhost)
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
CloudflaredTunnel object with public_url attribute
|
|
154
|
-
"""
|
|
155
|
-
cloudflared_path = download_cloudflared()
|
|
156
|
-
|
|
157
|
-
# Start cloudflared tunnel
|
|
158
|
-
cmd = [
|
|
159
|
-
str(cloudflared_path),
|
|
160
|
-
"tunnel",
|
|
161
|
-
"--url",
|
|
162
|
-
f"http://{host}:{port}",
|
|
163
|
-
]
|
|
164
|
-
|
|
165
|
-
process = subprocess.Popen(
|
|
166
|
-
cmd,
|
|
167
|
-
stdout=subprocess.PIPE,
|
|
168
|
-
stderr=subprocess.STDOUT,
|
|
169
|
-
text=True,
|
|
170
|
-
bufsize=1,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
# Parse output to get public URL
|
|
174
|
-
public_url = None
|
|
175
|
-
url_pattern = re.compile(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com")
|
|
176
|
-
|
|
177
|
-
def read_output():
|
|
178
|
-
nonlocal public_url
|
|
179
|
-
try:
|
|
180
|
-
for line in process.stdout:
|
|
181
|
-
if not line:
|
|
182
|
-
continue
|
|
183
|
-
line = line.strip()
|
|
184
|
-
if line:
|
|
185
|
-
logger.debug("[cloudflared] %s", line)
|
|
186
|
-
# Look for URL in the line
|
|
187
|
-
match = url_pattern.search(line)
|
|
188
|
-
if match:
|
|
189
|
-
public_url = match.group(0)
|
|
190
|
-
break
|
|
191
|
-
except Exception as e:
|
|
192
|
-
logger.error("[cloudflared] Error reading output: %s", e)
|
|
193
|
-
|
|
194
|
-
# Start reading output in a separate thread
|
|
195
|
-
output_thread = threading.Thread(target=read_output, daemon=True)
|
|
196
|
-
output_thread.start()
|
|
197
|
-
|
|
198
|
-
# Wait for URL to be available (max 30 seconds)
|
|
199
|
-
timeout = 30
|
|
200
|
-
start_time = time.time()
|
|
201
|
-
while public_url is None and (time.time() - start_time) < timeout:
|
|
202
|
-
if process.poll() is not None:
|
|
203
|
-
raise CloudflaredError("cloudflared process exited unexpectedly")
|
|
204
|
-
time.sleep(0.1)
|
|
205
|
-
|
|
206
|
-
if public_url is None:
|
|
207
|
-
process.terminate()
|
|
208
|
-
raise CloudflaredError("Failed to get public URL from cloudflared")
|
|
209
|
-
|
|
210
|
-
return CloudflaredTunnel(public_url=public_url, process=process)
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import stat
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.request import urlopen
|
|
11
|
+
|
|
12
|
+
from quraite.logger import get_logger
|
|
13
|
+
|
|
14
|
+
# Cloudflare Tunnel download URLs
|
|
15
|
+
CLOUDFLARED_RELEASES_URL = (
|
|
16
|
+
"https://github.com/cloudflare/cloudflared/releases/latest/download"
|
|
17
|
+
)
|
|
18
|
+
PLATFORMS = {
|
|
19
|
+
"darwin_x86_64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-darwin-amd64",
|
|
20
|
+
"darwin_arm64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-darwin-arm64",
|
|
21
|
+
"windows_x86_64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-windows-amd64.exe",
|
|
22
|
+
"linux_x86_64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-linux-amd64",
|
|
23
|
+
"linux_arm64": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-linux-arm64",
|
|
24
|
+
"linux_arm": f"{CLOUDFLARED_RELEASES_URL}/cloudflared-linux-arm",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CloudflaredError(Exception):
|
|
31
|
+
"""Base exception for cloudflared operations."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CloudflaredTunnel:
|
|
35
|
+
"""Represents a Cloudflare Tunnel connection."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, public_url: str, process: subprocess.Popen):
|
|
38
|
+
self.public_url = public_url
|
|
39
|
+
self._process = process
|
|
40
|
+
|
|
41
|
+
def disconnect(self):
|
|
42
|
+
"""Disconnect the tunnel."""
|
|
43
|
+
if self._process and self._process.poll() is None:
|
|
44
|
+
self._process.terminate()
|
|
45
|
+
try:
|
|
46
|
+
self._process.wait(timeout=5)
|
|
47
|
+
except subprocess.TimeoutExpired:
|
|
48
|
+
self._process.kill()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_system() -> str:
|
|
52
|
+
"""Get the system platform identifier."""
|
|
53
|
+
system = platform.system().lower()
|
|
54
|
+
machine = platform.machine().lower()
|
|
55
|
+
|
|
56
|
+
if system == "darwin":
|
|
57
|
+
if machine in ("arm64", "aarch64"):
|
|
58
|
+
return "darwin_arm64"
|
|
59
|
+
return "darwin_x86_64"
|
|
60
|
+
elif system == "windows":
|
|
61
|
+
return "windows_x86_64"
|
|
62
|
+
elif system == "linux":
|
|
63
|
+
if machine in ("arm64", "aarch64"):
|
|
64
|
+
return "linux_arm64"
|
|
65
|
+
elif machine.startswith("arm"):
|
|
66
|
+
return "linux_arm"
|
|
67
|
+
return "linux_x86_64"
|
|
68
|
+
else:
|
|
69
|
+
raise CloudflaredError(f"Unsupported platform: {system} {machine}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_cloudflared_path() -> Path:
|
|
73
|
+
"""Get the path where cloudflared binary should be stored."""
|
|
74
|
+
user_home = Path.home()
|
|
75
|
+
system = platform.system().lower()
|
|
76
|
+
|
|
77
|
+
if system == "darwin":
|
|
78
|
+
config_dir = user_home / "Library" / "Application Support" / "cloudflared"
|
|
79
|
+
elif system == "windows":
|
|
80
|
+
config_dir = user_home / "AppData" / "Local" / "cloudflared"
|
|
81
|
+
else:
|
|
82
|
+
config_dir = (
|
|
83
|
+
Path(os.environ.get("XDG_CONFIG_HOME", user_home / ".config"))
|
|
84
|
+
/ "cloudflared"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
if system == "windows":
|
|
90
|
+
return config_dir / "cloudflared.exe"
|
|
91
|
+
return config_dir / "cloudflared"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def download_cloudflared(force: bool = False) -> Path:
|
|
95
|
+
"""
|
|
96
|
+
Download cloudflared binary for the current platform.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
force: If True, re-download even if binary exists
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Path to the cloudflared binary
|
|
103
|
+
"""
|
|
104
|
+
# Check if cloudflared is already in PATH
|
|
105
|
+
cloudflared_cmd = (
|
|
106
|
+
"cloudflared.exe" if platform.system() == "windows" else "cloudflared"
|
|
107
|
+
)
|
|
108
|
+
if shutil.which(cloudflared_cmd) and not force:
|
|
109
|
+
return Path(shutil.which(cloudflared_cmd))
|
|
110
|
+
|
|
111
|
+
cloudflared_path = get_cloudflared_path()
|
|
112
|
+
system_key = get_system()
|
|
113
|
+
|
|
114
|
+
if cloudflared_path.exists() and not force:
|
|
115
|
+
# Check if binary is executable
|
|
116
|
+
if platform.system() != "windows":
|
|
117
|
+
st = os.stat(cloudflared_path)
|
|
118
|
+
os.chmod(cloudflared_path, st.st_mode | stat.S_IEXEC)
|
|
119
|
+
return cloudflared_path
|
|
120
|
+
|
|
121
|
+
if system_key not in PLATFORMS:
|
|
122
|
+
raise CloudflaredError(f"Unsupported platform: {system_key}")
|
|
123
|
+
|
|
124
|
+
download_url = PLATFORMS[system_key]
|
|
125
|
+
logger.info("Downloading cloudflared from %s...", download_url)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
with urlopen(download_url, timeout=30) as response:
|
|
129
|
+
with open(cloudflared_path, "wb") as f:
|
|
130
|
+
f.write(response.read())
|
|
131
|
+
|
|
132
|
+
# Make executable on Unix systems
|
|
133
|
+
if platform.system() != "windows":
|
|
134
|
+
st = os.stat(cloudflared_path)
|
|
135
|
+
os.chmod(cloudflared_path, st.st_mode | stat.S_IEXEC)
|
|
136
|
+
|
|
137
|
+
logger.info("Downloaded cloudflared to %s", cloudflared_path)
|
|
138
|
+
return cloudflared_path
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise CloudflaredError(f"Failed to download cloudflared: {e}") from e
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def connect(port: int, host: str = "localhost") -> CloudflaredTunnel:
|
|
145
|
+
"""
|
|
146
|
+
Create a Cloudflare Tunnel connection to the specified port.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
port: Local port to tunnel
|
|
150
|
+
host: Local host (default: localhost)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
CloudflaredTunnel object with public_url attribute
|
|
154
|
+
"""
|
|
155
|
+
cloudflared_path = download_cloudflared()
|
|
156
|
+
|
|
157
|
+
# Start cloudflared tunnel
|
|
158
|
+
cmd = [
|
|
159
|
+
str(cloudflared_path),
|
|
160
|
+
"tunnel",
|
|
161
|
+
"--url",
|
|
162
|
+
f"http://{host}:{port}",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
process = subprocess.Popen(
|
|
166
|
+
cmd,
|
|
167
|
+
stdout=subprocess.PIPE,
|
|
168
|
+
stderr=subprocess.STDOUT,
|
|
169
|
+
text=True,
|
|
170
|
+
bufsize=1,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Parse output to get public URL
|
|
174
|
+
public_url = None
|
|
175
|
+
url_pattern = re.compile(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com")
|
|
176
|
+
|
|
177
|
+
def read_output():
|
|
178
|
+
nonlocal public_url
|
|
179
|
+
try:
|
|
180
|
+
for line in process.stdout:
|
|
181
|
+
if not line:
|
|
182
|
+
continue
|
|
183
|
+
line = line.strip()
|
|
184
|
+
if line:
|
|
185
|
+
logger.debug("[cloudflared] %s", line)
|
|
186
|
+
# Look for URL in the line
|
|
187
|
+
match = url_pattern.search(line)
|
|
188
|
+
if match:
|
|
189
|
+
public_url = match.group(0)
|
|
190
|
+
break
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error("[cloudflared] Error reading output: %s", e)
|
|
193
|
+
|
|
194
|
+
# Start reading output in a separate thread
|
|
195
|
+
output_thread = threading.Thread(target=read_output, daemon=True)
|
|
196
|
+
output_thread.start()
|
|
197
|
+
|
|
198
|
+
# Wait for URL to be available (max 30 seconds)
|
|
199
|
+
timeout = 30
|
|
200
|
+
start_time = time.time()
|
|
201
|
+
while public_url is None and (time.time() - start_time) < timeout:
|
|
202
|
+
if process.poll() is not None:
|
|
203
|
+
raise CloudflaredError("cloudflared process exited unexpectedly")
|
|
204
|
+
time.sleep(0.1)
|
|
205
|
+
|
|
206
|
+
if public_url is None:
|
|
207
|
+
process.terminate()
|
|
208
|
+
raise CloudflaredError("Failed to get public URL from cloudflared")
|
|
209
|
+
|
|
210
|
+
return CloudflaredTunnel(public_url=public_url, process=process)
|