botversion-sdk 1.0.3__tar.gz → 1.0.5__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.
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/PKG-INFO +1 -2
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/__init__.py +15 -45
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/cli/detector.py +2 -1
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/cli/generator.py +7 -7
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/cli/init.py +2 -2
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/cli/writer.py +1 -1
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/client.py +9 -151
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/interceptor.py +9 -148
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/scanner.py +20 -54
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/setup.py +1 -1
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk.egg-info/PKG-INFO +1 -2
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk.egg-info/SOURCES.txt +0 -1
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/pyproject.toml +2 -4
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/setup.py +1 -1
- botversion_sdk-1.0.3/botversion_sdk.egg-info/requires.txt +0 -1
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk/cli/prompts.py +0 -0
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk.egg-info/dependency_links.txt +0 -0
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk.egg-info/entry_points.txt +0 -0
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/botversion_sdk.egg-info/top_level.txt +0 -0
- {botversion_sdk-1.0.3 → botversion_sdk-1.0.5}/setup.cfg +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: botversion-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: BotVersion AI Agent SDK for FastAPI, Flask, and Django
|
|
5
5
|
Home-page: https://github.com/botversion/botversion-sdk-python
|
|
6
6
|
Author: BotVersion
|
|
7
7
|
Author-email: Saurav Dhakal <sauravdhakal828@gmail.com>
|
|
8
8
|
Requires-Python: >=3.7
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: websocket-client>=1.0.0
|
|
11
10
|
Dynamic: author
|
|
12
11
|
Dynamic: description-content-type
|
|
13
12
|
Dynamic: home-page
|
|
@@ -36,20 +36,25 @@ def init(app=None, api_key=None, **options):
|
|
|
36
36
|
"""
|
|
37
37
|
global _initialized, _client, _options, _app
|
|
38
38
|
|
|
39
|
-
if not api_key:
|
|
40
|
-
print("[BotVersion SDK] ❌ api_key is required.")
|
|
41
|
-
return
|
|
42
|
-
|
|
43
39
|
# Restore from builtins if module was re-imported after hot reload
|
|
44
40
|
if getattr(builtins, "_botversion_client", None):
|
|
45
41
|
_client = builtins._botversion_client
|
|
46
42
|
_options = builtins._botversion_options
|
|
47
43
|
_initialized = True
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
# Re-attach interceptor after hot reload
|
|
45
|
+
framework = _detect_framework(app)
|
|
46
|
+
if framework and _client:
|
|
47
|
+
interceptor_options = {
|
|
48
|
+
"exclude": _options.get("exclude", []),
|
|
49
|
+
"api_prefix": _options.get("api_prefix", None),
|
|
50
|
+
"debug": _options.get("debug", False),
|
|
51
|
+
}
|
|
52
|
+
if framework == "fastapi":
|
|
53
|
+
attach_fastapi_interceptor(app, _client, interceptor_options)
|
|
54
|
+
elif framework == "flask":
|
|
55
|
+
attach_flask_interceptor(app, _client, interceptor_options)
|
|
56
|
+
elif framework == "django":
|
|
57
|
+
attach_django_interceptor(_client, interceptor_options)
|
|
53
58
|
return
|
|
54
59
|
|
|
55
60
|
_initialized = True
|
|
@@ -63,14 +68,12 @@ def init(app=None, api_key=None, **options):
|
|
|
63
68
|
framework = _detect_framework(app)
|
|
64
69
|
|
|
65
70
|
if not framework:
|
|
66
|
-
print("[BotVersion SDK] ❌ Could not detect framework.")
|
|
67
|
-
print("[BotVersion SDK] ❌ Make sure FastAPI, Flask, or Django is installed.")
|
|
68
71
|
_initialized = False
|
|
69
72
|
return
|
|
70
73
|
|
|
71
74
|
_client = BotVersionClient({
|
|
72
75
|
"api_key": api_key,
|
|
73
|
-
"platform_url": options.get("platform_url", "
|
|
76
|
+
"platform_url": options.get("platform_url", "https://botversion.com"),
|
|
74
77
|
"debug": debug,
|
|
75
78
|
"timeout": options.get("timeout", 30),
|
|
76
79
|
"flush_delay": options.get("flush_delay", 3),
|
|
@@ -80,9 +83,6 @@ def init(app=None, api_key=None, **options):
|
|
|
80
83
|
builtins._botversion_client = _client
|
|
81
84
|
builtins._botversion_options = _options
|
|
82
85
|
|
|
83
|
-
if debug:
|
|
84
|
-
print(f"[BotVersion SDK] ✅ Framework detected: {framework}")
|
|
85
|
-
|
|
86
86
|
interceptor_options = {
|
|
87
87
|
"exclude": options.get("exclude", []),
|
|
88
88
|
"api_prefix": options.get("api_prefix", None),
|
|
@@ -97,49 +97,26 @@ def init(app=None, api_key=None, **options):
|
|
|
97
97
|
elif framework == "django":
|
|
98
98
|
attach_django_interceptor(_client, interceptor_options)
|
|
99
99
|
else:
|
|
100
|
-
print(f"[BotVersion SDK] ❌ Unsupported framework: {framework}")
|
|
101
100
|
return
|
|
102
101
|
|
|
103
|
-
if debug:
|
|
104
|
-
print("[BotVersion SDK] ✅ Runtime interceptor attached")
|
|
105
|
-
|
|
106
102
|
# ── Static scan (delayed 500ms — let app finish registering routes) ──────
|
|
107
103
|
def _run_scan():
|
|
108
104
|
try:
|
|
109
105
|
endpoints = []
|
|
110
106
|
|
|
111
107
|
if app is not None:
|
|
112
|
-
print(f"[BotVersion SDK] Scanning {framework} routes...")
|
|
113
108
|
endpoints = scan_routes(app, framework)
|
|
114
|
-
print(f"[BotVersion SDK] Found {len(endpoints)} {framework} routes")
|
|
115
|
-
|
|
116
|
-
if debug:
|
|
117
|
-
import json
|
|
118
|
-
print(f"[BotVersion SDK] Endpoints: {json.dumps(endpoints, indent=2)}")
|
|
119
|
-
|
|
120
|
-
if len(endpoints) == 0:
|
|
121
|
-
print("[BotVersion SDK] ⚠ No endpoints found.")
|
|
122
|
-
print("[BotVersion SDK] ⚠ Make sure routes are defined BEFORE botversion_sdk.init()")
|
|
123
109
|
|
|
124
110
|
elif framework == "django":
|
|
125
|
-
print("[BotVersion SDK] Scanning Django routes...")
|
|
126
111
|
endpoints = scan_routes(None, "django")
|
|
127
|
-
print(f"[BotVersion SDK] Found {len(endpoints)} Django routes")
|
|
128
112
|
|
|
129
|
-
if len(endpoints) == 0:
|
|
130
|
-
print("[BotVersion SDK] ⚠ No Django routes found.")
|
|
131
|
-
print("[BotVersion SDK] ⚠ Make sure botversion_sdk.init() is called AFTER Django is fully loaded.")
|
|
132
113
|
else:
|
|
133
|
-
print("[BotVersion SDK] ❌ No routes to scan.")
|
|
134
114
|
return
|
|
135
115
|
|
|
136
116
|
if endpoints:
|
|
137
|
-
print(f"[BotVersion SDK] Sending {len(endpoints)} endpoints to platform...")
|
|
138
117
|
_client.register_endpoints_now(endpoints)
|
|
139
|
-
print(f"[BotVersion SDK] ✅ Static scan complete — {len(endpoints)} endpoints registered")
|
|
140
118
|
|
|
141
119
|
except Exception as e:
|
|
142
|
-
print(f"[BotVersion SDK] ❌ Scan error: {e}")
|
|
143
120
|
if debug:
|
|
144
121
|
import traceback
|
|
145
122
|
traceback.print_exc()
|
|
@@ -147,14 +124,7 @@ def init(app=None, api_key=None, **options):
|
|
|
147
124
|
cwd = options.get("cwd", os.getcwd())
|
|
148
125
|
route_patterns = scan_frontend_routes(cwd)
|
|
149
126
|
if route_patterns:
|
|
150
|
-
print(f"[BotVersion SDK] Found {len(route_patterns)} frontend route patterns")
|
|
151
127
|
_client.register_route_patterns(route_patterns)
|
|
152
|
-
print("[BotVersion SDK] ✅ Route patterns registered")
|
|
153
|
-
|
|
154
|
-
print("[BotVersion SDK] ✅ Initialization complete")
|
|
155
|
-
|
|
156
|
-
# ── Connect WebSocket immediately — don't wait for first request ──────
|
|
157
|
-
_client.connect()
|
|
158
128
|
|
|
159
129
|
if framework == "flask":
|
|
160
130
|
@app.after_request
|
|
@@ -259,7 +259,8 @@ def score_django_file(content, filepath):
|
|
|
259
259
|
if re.search(r'(test_|_test|conftest)', filename): score -= 10
|
|
260
260
|
|
|
261
261
|
# Filename bonus
|
|
262
|
-
if filename
|
|
262
|
+
if filename == 'wsgi.py': score += 6
|
|
263
|
+
if filename == 'asgi.py': score += 4
|
|
263
264
|
if filename == 'manage.py': score += 3
|
|
264
265
|
if filename == '__init__.py': score += 1
|
|
265
266
|
|
|
@@ -67,7 +67,7 @@ def generate_fastapi_init(info, api_key):
|
|
|
67
67
|
botversion_sdk.init(
|
|
68
68
|
{app_var},
|
|
69
69
|
api_key=os.environ.get("BOTVERSION_API_KEY"),
|
|
70
|
-
platform_url=os.environ.get("BOTVERSION_PLATFORM_URL", "https://
|
|
70
|
+
platform_url=os.environ.get("BOTVERSION_PLATFORM_URL", "https://botversion.com"),
|
|
71
71
|
routes_dir={routes_dir},
|
|
72
72
|
)
|
|
73
73
|
"""
|
|
@@ -90,7 +90,7 @@ def generate_flask_init(info, api_key):
|
|
|
90
90
|
botversion_sdk.init(
|
|
91
91
|
{app_var},
|
|
92
92
|
api_key=os.environ.get("BOTVERSION_API_KEY"),
|
|
93
|
-
platform_url=os.environ.get("BOTVERSION_PLATFORM_URL", "https://
|
|
93
|
+
platform_url=os.environ.get("BOTVERSION_PLATFORM_URL", "https://botversion.com"),
|
|
94
94
|
routes_dir={routes_dir},
|
|
95
95
|
)
|
|
96
96
|
"""
|
|
@@ -111,7 +111,7 @@ import botversion_sdk
|
|
|
111
111
|
|
|
112
112
|
botversion_sdk.init(
|
|
113
113
|
api_key=os.environ.get("BOTVERSION_API_KEY"),
|
|
114
|
-
platform_url=os.environ.get("BOTVERSION_PLATFORM_URL", "https://
|
|
114
|
+
platform_url=os.environ.get("BOTVERSION_PLATFORM_URL", "https://botversion.com"),
|
|
115
115
|
routes_dir={routes_dir},
|
|
116
116
|
)
|
|
117
117
|
""".strip()
|
|
@@ -130,7 +130,7 @@ Tornado support is coming soon. For now, add this manually:
|
|
|
130
130
|
# After defining your handlers:
|
|
131
131
|
botversion_sdk.init(api_key=os.environ.get("BOTVERSION_API_KEY"))
|
|
132
132
|
|
|
133
|
-
# See: https://
|
|
133
|
+
# See: https://botversion.com/docs
|
|
134
134
|
""",
|
|
135
135
|
"aiohttp": f"""
|
|
136
136
|
aiohttp support is coming soon. For now, add this manually:
|
|
@@ -140,7 +140,7 @@ aiohttp support is coming soon. For now, add this manually:
|
|
|
140
140
|
|
|
141
141
|
botversion_sdk.init(api_key=os.environ.get("BOTVERSION_API_KEY"))
|
|
142
142
|
|
|
143
|
-
# See: https://
|
|
143
|
+
# See: https://botversion.com/docs
|
|
144
144
|
""",
|
|
145
145
|
"sanic": f"""
|
|
146
146
|
Sanic support is coming soon. For now, add this manually:
|
|
@@ -150,7 +150,7 @@ Sanic support is coming soon. For now, add this manually:
|
|
|
150
150
|
|
|
151
151
|
botversion_sdk.init(app, api_key=os.environ.get("BOTVERSION_API_KEY"))
|
|
152
152
|
|
|
153
|
-
# See: https://
|
|
153
|
+
# See: https://botversion.com/docs
|
|
154
154
|
""",
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -158,7 +158,7 @@ Sanic support is coming soon. For now, add this manually:
|
|
|
158
158
|
framework,
|
|
159
159
|
"""
|
|
160
160
|
This framework is not yet supported automatically.
|
|
161
|
-
Visit https://
|
|
161
|
+
Visit https://botversion.com/docs for manual setup instructions.
|
|
162
162
|
"""
|
|
163
163
|
)
|
|
164
164
|
|
|
@@ -158,7 +158,7 @@ def main():
|
|
|
158
158
|
log()
|
|
159
159
|
log(" Usage: botversion-init --key YOUR_WORKSPACE_KEY")
|
|
160
160
|
log()
|
|
161
|
-
log(" Get your key from:
|
|
161
|
+
log(" Get your key from: https://botversion.com/workspace/settings")
|
|
162
162
|
log()
|
|
163
163
|
sys.exit(1)
|
|
164
164
|
|
|
@@ -170,7 +170,7 @@ def main():
|
|
|
170
170
|
try:
|
|
171
171
|
import urllib.request
|
|
172
172
|
import json as _json
|
|
173
|
-
url = f"
|
|
173
|
+
url = f"https://botversion.com/api/sdk/project-info?workspaceKey={args.key}"
|
|
174
174
|
with urllib.request.urlopen(url) as response:
|
|
175
175
|
project_info = _json.loads(response.read().decode())
|
|
176
176
|
success(f"Project found — ID: {project_info.get('projectId')}")
|
|
@@ -549,7 +549,7 @@ def write_summary(changes):
|
|
|
549
549
|
lines.append("")
|
|
550
550
|
|
|
551
551
|
lines.append(" Next: Restart your server and test the chat widget.")
|
|
552
|
-
lines.append(" Docs: https://
|
|
552
|
+
lines.append(" Docs: https://botversion.com/docs")
|
|
553
553
|
lines.append("")
|
|
554
554
|
|
|
555
555
|
return "\n".join(lines)
|
|
@@ -6,20 +6,12 @@ import urllib.parse
|
|
|
6
6
|
import urllib.error
|
|
7
7
|
import atexit
|
|
8
8
|
import time
|
|
9
|
-
import logging
|
|
10
|
-
|
|
11
|
-
logging.getLogger("websocket").setLevel(logging.CRITICAL)
|
|
12
9
|
|
|
13
10
|
class BotVersionClient:
|
|
14
11
|
|
|
15
12
|
def __init__(self, options):
|
|
16
13
|
self.api_key = options["api_key"]
|
|
17
|
-
platform_url = options.get("platform_url", "
|
|
18
|
-
|
|
19
|
-
# Force IPv4 — on Windows, localhost resolves to ::1 (IPv6) in browsers
|
|
20
|
-
# but Python's urllib uses 127.0.0.1 (IPv4), causing connection timeouts
|
|
21
|
-
platform_url = platform_url.replace("http://localhost", "http://127.0.0.1")
|
|
22
|
-
platform_url = platform_url.replace("https://localhost", "https://127.0.0.1")
|
|
14
|
+
platform_url = options.get("platform_url", "https://botversion.com")
|
|
23
15
|
|
|
24
16
|
self.platform_url = platform_url
|
|
25
17
|
self.debug = options.get("debug", False)
|
|
@@ -32,133 +24,12 @@ class BotVersionClient:
|
|
|
32
24
|
self._lock = threading.Lock()
|
|
33
25
|
atexit.register(self._flush)
|
|
34
26
|
|
|
35
|
-
# WebSocket state
|
|
36
|
-
self._ws = None
|
|
37
|
-
self._executor = None
|
|
38
|
-
self._pending_calls = {}
|
|
39
|
-
|
|
40
|
-
# ── Set executor (called by interceptor after attach) ────────────────────────
|
|
41
|
-
def set_executor(self, executor_fn):
|
|
42
|
-
self._executor = executor_fn
|
|
43
|
-
if self.debug:
|
|
44
|
-
print("[BotVersion SDK] ✅ Executor registered")
|
|
45
|
-
|
|
46
|
-
# ── WebSocket connection ──────────────────────────────────────────────────────
|
|
47
|
-
def connect(self):
|
|
48
|
-
# ✅ No warmup needed — ws-server.js is always running
|
|
49
|
-
t = threading.Thread(target=self._ws_loop, daemon=True)
|
|
50
|
-
t.start()
|
|
51
|
-
|
|
52
|
-
def _ws_loop(self):
|
|
53
|
-
ws_url = self.platform_url \
|
|
54
|
-
.replace("https://", "wss://") \
|
|
55
|
-
.replace("http://", "ws://") \
|
|
56
|
-
.replace(":3000", ":3001")
|
|
57
|
-
# ✅ ws-server.js accepts connections at root path
|
|
58
|
-
ws_url = ws_url + "?apiKey=" + urllib.parse.quote(self.api_key)
|
|
59
|
-
|
|
60
|
-
while True:
|
|
61
|
-
try:
|
|
62
|
-
import websocket
|
|
63
|
-
ws = websocket.WebSocketApp(
|
|
64
|
-
ws_url,
|
|
65
|
-
on_open=self._on_ws_open,
|
|
66
|
-
on_message=self._on_ws_message,
|
|
67
|
-
on_error=self._on_ws_error,
|
|
68
|
-
on_close=self._on_ws_close,
|
|
69
|
-
)
|
|
70
|
-
with self._lock:
|
|
71
|
-
self._ws = ws
|
|
72
|
-
ws.run_forever(ping_interval=30, ping_timeout=10)
|
|
73
|
-
except ImportError:
|
|
74
|
-
print("[BotVersion SDK] ❌ websocket-client not installed. Run: pip install websocket-client")
|
|
75
|
-
break
|
|
76
|
-
except Exception as e:
|
|
77
|
-
if self.debug:
|
|
78
|
-
print(f"[BotVersion SDK] ⚠ WebSocket error: {e}")
|
|
79
|
-
if self.debug:
|
|
80
|
-
print("[BotVersion SDK] Reconnecting in 5 seconds...")
|
|
81
|
-
time.sleep(5)
|
|
82
|
-
|
|
83
|
-
def _on_ws_open(self, ws):
|
|
84
|
-
if self.debug:
|
|
85
|
-
print("[BotVersion SDK] ✅ WebSocket connected to platform")
|
|
86
|
-
ws.send(json.dumps({
|
|
87
|
-
"type": "IDENTIFY",
|
|
88
|
-
"apiKey": self.api_key,
|
|
89
|
-
}))
|
|
90
|
-
|
|
91
|
-
def _on_ws_message(self, ws, message):
|
|
92
|
-
try:
|
|
93
|
-
data = json.loads(message)
|
|
94
|
-
msg_type = data.get("type")
|
|
95
|
-
|
|
96
|
-
if msg_type == "EXECUTE_CALL":
|
|
97
|
-
threading.Thread(
|
|
98
|
-
target=self._handle_execute_call,
|
|
99
|
-
args=(data,),
|
|
100
|
-
daemon=True,
|
|
101
|
-
).start()
|
|
102
|
-
|
|
103
|
-
except Exception as e:
|
|
104
|
-
if self.debug:
|
|
105
|
-
print(f"[BotVersion SDK] ⚠ Error handling message: {e}")
|
|
106
|
-
|
|
107
|
-
def _handle_execute_call(self, data):
|
|
108
|
-
call_id = data.get("callId")
|
|
109
|
-
method = data.get("method")
|
|
110
|
-
path = data.get("path")
|
|
111
|
-
body = data.get("body")
|
|
112
|
-
cookies = data.get("cookies", "")
|
|
113
|
-
headers = data.get("headers", {})
|
|
114
|
-
base_url = data.get("baseUrl", "http://127.0.0.1:8000")
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
if not self._executor:
|
|
118
|
-
raise RuntimeError("No executor registered")
|
|
119
|
-
|
|
120
|
-
result = self._executor(method, path, body, cookies, headers, base_url)
|
|
121
|
-
|
|
122
|
-
except Exception as e:
|
|
123
|
-
result = {
|
|
124
|
-
"status": 500,
|
|
125
|
-
"ok": False,
|
|
126
|
-
"data": {"error": str(e)},
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
# Send result back to platform
|
|
130
|
-
try:
|
|
131
|
-
with self._lock:
|
|
132
|
-
ws = self._ws
|
|
133
|
-
if ws:
|
|
134
|
-
ws.send(json.dumps({
|
|
135
|
-
"type": "CALL_RESULT",
|
|
136
|
-
"callId": call_id,
|
|
137
|
-
"result": result,
|
|
138
|
-
}))
|
|
139
|
-
except Exception as e:
|
|
140
|
-
if self.debug:
|
|
141
|
-
print(f"[BotVersion SDK] ⚠ Failed to send result: {e}")
|
|
142
|
-
|
|
143
|
-
def _on_ws_error(self, ws, error):
|
|
144
|
-
if self.debug:
|
|
145
|
-
print(f"[BotVersion SDK] ⚠ WebSocket error: {error}")
|
|
146
|
-
|
|
147
|
-
def _on_ws_close(self, ws, close_status_code, close_msg):
|
|
148
|
-
if self.debug:
|
|
149
|
-
print("[BotVersion SDK] WebSocket closed — will reconnect")
|
|
150
|
-
with self._lock:
|
|
151
|
-
self._ws = None
|
|
152
|
-
|
|
153
27
|
# ── Register endpoints (batched) ─────────────────────────────────────────
|
|
154
28
|
|
|
155
29
|
def register_endpoints(self, endpoints):
|
|
156
30
|
if not endpoints:
|
|
157
31
|
return
|
|
158
32
|
|
|
159
|
-
if self.debug:
|
|
160
|
-
print(f"[BotVersion SDK] Queuing {len(endpoints)} endpoints for registration")
|
|
161
|
-
|
|
162
33
|
with self._lock:
|
|
163
34
|
self._queue.extend(endpoints)
|
|
164
35
|
|
|
@@ -175,11 +46,9 @@ class BotVersionClient:
|
|
|
175
46
|
"workspaceKey": self.api_key,
|
|
176
47
|
"endpoints": endpoints,
|
|
177
48
|
})
|
|
178
|
-
if self.debug:
|
|
179
|
-
print(f"[BotVersion SDK] ✅ Registered {len(endpoints)} endpoints")
|
|
180
49
|
return data
|
|
181
|
-
except Exception
|
|
182
|
-
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
183
52
|
|
|
184
53
|
# ── Flush batch ──────────────────────────────────────────────────────────
|
|
185
54
|
|
|
@@ -191,20 +60,13 @@ class BotVersionClient:
|
|
|
191
60
|
to_send = self._queue[:]
|
|
192
61
|
self._queue = []
|
|
193
62
|
|
|
194
|
-
if self.debug:
|
|
195
|
-
print(f"[BotVersion SDK] Flushing {len(to_send)} endpoints to platform")
|
|
196
|
-
|
|
197
63
|
try:
|
|
198
64
|
data = self._post("/api/sdk/register-endpoints", {
|
|
199
65
|
"workspaceKey": self.api_key,
|
|
200
66
|
"endpoints": to_send,
|
|
201
67
|
})
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
print(f"[BotVersion SDK] Registered {succeeded} endpoints successfully")
|
|
205
|
-
except Exception as e:
|
|
206
|
-
if self.debug:
|
|
207
|
-
print(f"[BotVersion SDK] ⚠ Failed to register endpoints: {e}")
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
208
70
|
|
|
209
71
|
# ── Update single endpoint (runtime interceptor) ─────────────────────────
|
|
210
72
|
|
|
@@ -218,9 +80,8 @@ class BotVersionClient:
|
|
|
218
80
|
"responseBody": endpoint.get("response_body"),
|
|
219
81
|
"detectedBy": endpoint.get("detected_by", "runtime"),
|
|
220
82
|
})
|
|
221
|
-
except Exception
|
|
222
|
-
|
|
223
|
-
print(f"[BotVersion SDK] ⚠ Failed to update endpoint: {e}")
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
224
85
|
|
|
225
86
|
|
|
226
87
|
# ── Register frontend route patterns ─────────────────────────────────────────
|
|
@@ -233,11 +94,8 @@ class BotVersionClient:
|
|
|
233
94
|
"workspaceKey": self.api_key,
|
|
234
95
|
"patterns": patterns,
|
|
235
96
|
})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
except Exception as e:
|
|
239
|
-
if self.debug:
|
|
240
|
-
print(f"[BotVersion SDK] ⚠ Failed to register route patterns: {e}")
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
241
99
|
|
|
242
100
|
# ── Get all endpoints ────────────────────────────────────────────────────
|
|
243
101
|
|
|
@@ -3,126 +3,6 @@ import re
|
|
|
3
3
|
import json
|
|
4
4
|
import threading
|
|
5
5
|
|
|
6
|
-
def make_internal_request(method, path, body, cookies, headers, base_url="http://127.0.0.1:8000"):
|
|
7
|
-
"""
|
|
8
|
-
Makes an internal HTTP request to the user's own backend.
|
|
9
|
-
Forwards cookies so the backend identifies the user correctly.
|
|
10
|
-
Works for all auth types — JWT, session, cookie-based.
|
|
11
|
-
"""
|
|
12
|
-
import urllib.request
|
|
13
|
-
import urllib.error
|
|
14
|
-
import json
|
|
15
|
-
|
|
16
|
-
# Build the full URL — calling the user's own backend internally
|
|
17
|
-
url = f"{base_url}{path}"
|
|
18
|
-
|
|
19
|
-
body_bytes = json.dumps(body).encode("utf-8") if body else None
|
|
20
|
-
|
|
21
|
-
req = urllib.request.Request(
|
|
22
|
-
url,
|
|
23
|
-
data=body_bytes,
|
|
24
|
-
method=method.upper(),
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# Forward all original headers
|
|
28
|
-
req.add_header("Content-Type", "application/json")
|
|
29
|
-
|
|
30
|
-
# Forward cookies — this is what identifies the user
|
|
31
|
-
if cookies:
|
|
32
|
-
req.add_header("Cookie", cookies)
|
|
33
|
-
|
|
34
|
-
# Forward auth header if present
|
|
35
|
-
auth_header = headers.get("authorization") or headers.get("Authorization")
|
|
36
|
-
if auth_header:
|
|
37
|
-
req.add_header("Authorization", auth_header)
|
|
38
|
-
|
|
39
|
-
# Forward CSRF token if present
|
|
40
|
-
csrf = (
|
|
41
|
-
headers.get("x-csrftoken")
|
|
42
|
-
or headers.get("X-CSRFToken")
|
|
43
|
-
or headers.get("x-xsrf-token")
|
|
44
|
-
or headers.get("X-XSRF-TOKEN")
|
|
45
|
-
)
|
|
46
|
-
if csrf:
|
|
47
|
-
req.add_header("X-CSRFToken", csrf)
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
# Follow redirects manually — urllib does not follow redirects for POST
|
|
51
|
-
max_redirects = 5
|
|
52
|
-
current_req = req
|
|
53
|
-
current_url = url
|
|
54
|
-
|
|
55
|
-
for _ in range(max_redirects):
|
|
56
|
-
try:
|
|
57
|
-
with urllib.request.urlopen(current_req, timeout=30) as res:
|
|
58
|
-
raw = res.read().decode("utf-8")
|
|
59
|
-
try:
|
|
60
|
-
data = json.loads(raw)
|
|
61
|
-
except Exception:
|
|
62
|
-
data = {"raw": raw}
|
|
63
|
-
return {
|
|
64
|
-
"status": res.status,
|
|
65
|
-
"ok": 200 <= res.status < 300,
|
|
66
|
-
"data": data,
|
|
67
|
-
}
|
|
68
|
-
except urllib.error.HTTPError as e:
|
|
69
|
-
if e.code in (301, 302, 303, 307, 308):
|
|
70
|
-
redirect_url = e.headers.get("Location")
|
|
71
|
-
if not redirect_url:
|
|
72
|
-
raise
|
|
73
|
-
|
|
74
|
-
# Handle relative redirects
|
|
75
|
-
if redirect_url.startswith("/"):
|
|
76
|
-
from urllib.parse import urlparse
|
|
77
|
-
parsed = urlparse(current_url)
|
|
78
|
-
redirect_url = f"{parsed.scheme}://{parsed.netloc}{redirect_url}"
|
|
79
|
-
|
|
80
|
-
# 307 and 308 keep the original method and body
|
|
81
|
-
# 301, 302, 303 switch to GET with no body
|
|
82
|
-
if e.code in (307, 308):
|
|
83
|
-
new_req = urllib.request.Request(
|
|
84
|
-
redirect_url,
|
|
85
|
-
data=current_req.data,
|
|
86
|
-
method=current_req.get_method(),
|
|
87
|
-
)
|
|
88
|
-
else:
|
|
89
|
-
new_req = urllib.request.Request(
|
|
90
|
-
redirect_url,
|
|
91
|
-
data=None,
|
|
92
|
-
method="GET",
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Forward headers to redirected request
|
|
96
|
-
new_req.add_header("Content-Type", "application/json")
|
|
97
|
-
if cookies:
|
|
98
|
-
new_req.add_header("Cookie", cookies)
|
|
99
|
-
if auth_header:
|
|
100
|
-
new_req.add_header("Authorization", auth_header)
|
|
101
|
-
if csrf:
|
|
102
|
-
new_req.add_header("X-CSRFToken", csrf)
|
|
103
|
-
|
|
104
|
-
current_req = new_req
|
|
105
|
-
current_url = redirect_url
|
|
106
|
-
else:
|
|
107
|
-
raise
|
|
108
|
-
except urllib.error.HTTPError as e:
|
|
109
|
-
raw = e.read().decode("utf-8")
|
|
110
|
-
try:
|
|
111
|
-
data = json.loads(raw)
|
|
112
|
-
except Exception:
|
|
113
|
-
data = {"error": raw}
|
|
114
|
-
return {
|
|
115
|
-
"status": e.code,
|
|
116
|
-
"ok": False,
|
|
117
|
-
"data": data,
|
|
118
|
-
}
|
|
119
|
-
except Exception as e:
|
|
120
|
-
return {
|
|
121
|
-
"status": 500,
|
|
122
|
-
"ok": False,
|
|
123
|
-
"data": {"error": str(e)},
|
|
124
|
-
}
|
|
125
|
-
|
|
126
6
|
# Paths to always ignore
|
|
127
7
|
IGNORE_PATHS = [
|
|
128
8
|
"/health",
|
|
@@ -247,8 +127,7 @@ def report_endpoint(client, method, path, body_structure, options):
|
|
|
247
127
|
"detected_by": "runtime",
|
|
248
128
|
})
|
|
249
129
|
except Exception as e:
|
|
250
|
-
|
|
251
|
-
print(f"[BotVersion SDK] ⚠ Failed to report endpoint: {e}")
|
|
130
|
+
print(f"[botversion] update_endpoint failed: {e}")
|
|
252
131
|
|
|
253
132
|
t = threading.Thread(target=_send, daemon=True)
|
|
254
133
|
t.start()
|
|
@@ -267,6 +146,8 @@ def attach_fastapi_interceptor(app, client, options):
|
|
|
267
146
|
path = request.url.path
|
|
268
147
|
method = request.method.upper()
|
|
269
148
|
|
|
149
|
+
response = await call_next(request)
|
|
150
|
+
|
|
270
151
|
if not should_ignore(path, options.get("exclude")):
|
|
271
152
|
if not options.get("api_prefix") or path.startswith(options["api_prefix"]):
|
|
272
153
|
try:
|
|
@@ -279,22 +160,15 @@ def attach_fastapi_interceptor(app, client, options):
|
|
|
279
160
|
except Exception:
|
|
280
161
|
body_structure = None
|
|
281
162
|
|
|
282
|
-
|
|
163
|
+
if response.status_code < 500:
|
|
164
|
+
report_endpoint(client, method, path, body_structure, options)
|
|
283
165
|
|
|
284
|
-
return
|
|
166
|
+
return response
|
|
285
167
|
|
|
286
168
|
app.add_middleware(BotVersionMiddleware)
|
|
287
169
|
|
|
288
|
-
if options.get("debug"):
|
|
289
|
-
print("[BotVersion SDK] ✅ FastAPI middleware attached")
|
|
290
|
-
|
|
291
|
-
# Register executor so WebSocket can make internal calls
|
|
292
|
-
client.set_executor(lambda method, path, body, cookies, headers, base_url:
|
|
293
|
-
make_internal_request(method, path, body, cookies, headers, base_url)
|
|
294
|
-
)
|
|
295
|
-
|
|
296
170
|
except ImportError:
|
|
297
|
-
|
|
171
|
+
pass
|
|
298
172
|
|
|
299
173
|
|
|
300
174
|
# ── Flask middleware ──────────────────────────────────────────────────────────
|
|
@@ -320,15 +194,8 @@ def attach_flask_interceptor(app, client, options):
|
|
|
320
194
|
|
|
321
195
|
report_endpoint(client, method, path, body_structure, options)
|
|
322
196
|
|
|
323
|
-
if options.get("debug"):
|
|
324
|
-
print("[BotVersion SDK] ✅ Flask interceptor attached")
|
|
325
|
-
|
|
326
|
-
client.set_executor(lambda method, path, body, cookies, headers, base_url:
|
|
327
|
-
make_internal_request(method, path, body, cookies, headers, base_url)
|
|
328
|
-
)
|
|
329
|
-
|
|
330
197
|
except ImportError:
|
|
331
|
-
|
|
198
|
+
pass
|
|
332
199
|
|
|
333
200
|
|
|
334
201
|
# ── Django middleware ─────────────────────────────────────────────────────────
|
|
@@ -383,14 +250,8 @@ def attach_django_interceptor(client, options):
|
|
|
383
250
|
else:
|
|
384
251
|
settings.MIDDLEWARE.insert(0, middleware_path)
|
|
385
252
|
|
|
386
|
-
if options.get("debug"):
|
|
387
|
-
print("[BotVersion SDK] ✅ Django middleware attached")
|
|
388
|
-
|
|
389
253
|
BotVersionDjangoMiddleware._client = client
|
|
390
254
|
BotVersionDjangoMiddleware._options = options
|
|
391
|
-
client.set_executor(lambda method, path, body, cookies, headers, base_url:
|
|
392
|
-
make_internal_request(method, path, body, cookies, headers, base_url)
|
|
393
|
-
)
|
|
394
255
|
|
|
395
256
|
except ImportError:
|
|
396
|
-
|
|
257
|
+
pass
|
|
@@ -14,13 +14,6 @@ def scan_routes(app, framework):
|
|
|
14
14
|
elif framework == "django":
|
|
15
15
|
result = scan_django_routes()
|
|
16
16
|
|
|
17
|
-
# ADD THIS
|
|
18
|
-
print(f"\n[DEBUG] ===== SCAN SUMMARY =====")
|
|
19
|
-
for ep in result:
|
|
20
|
-
status = "✅" if ep.get("requestBody") else "❌ NULL"
|
|
21
|
-
print(f"[DEBUG] {status} {ep['method']:6} {ep['path']} → {ep.get('requestBody')}")
|
|
22
|
-
print(f"[DEBUG] ==========================\n")
|
|
23
|
-
|
|
24
17
|
return result
|
|
25
18
|
|
|
26
19
|
|
|
@@ -64,8 +57,8 @@ def scan_fastapi_routes(app):
|
|
|
64
57
|
"detectedBy": "static-scan",
|
|
65
58
|
})
|
|
66
59
|
|
|
67
|
-
except Exception
|
|
68
|
-
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
69
62
|
|
|
70
63
|
return endpoints
|
|
71
64
|
|
|
@@ -119,10 +112,8 @@ def scan_flask_routes(app):
|
|
|
119
112
|
"detectedBy": "static-scan",
|
|
120
113
|
})
|
|
121
114
|
|
|
122
|
-
except Exception
|
|
123
|
-
|
|
124
|
-
print(f"[BotVersion SDK] ⚠ Flask scan error: {e}")
|
|
125
|
-
traceback.print_exc()
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
126
117
|
|
|
127
118
|
return endpoints
|
|
128
119
|
|
|
@@ -151,8 +142,8 @@ def scan_django_routes():
|
|
|
151
142
|
|
|
152
143
|
resolver = get_resolver()
|
|
153
144
|
_walk_django_patterns(resolver.url_patterns, "", endpoints, seen)
|
|
154
|
-
except Exception
|
|
155
|
-
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
156
147
|
|
|
157
148
|
return endpoints
|
|
158
149
|
|
|
@@ -221,11 +212,10 @@ def extract_drf_schema(callback, method):
|
|
|
221
212
|
if required:
|
|
222
213
|
result["required"] = required
|
|
223
214
|
|
|
224
|
-
print(f"[BotVersion SDK] ✅ DRF schema extracted for {method}: {list(properties.keys())}")
|
|
225
215
|
return result
|
|
226
216
|
|
|
227
|
-
except Exception
|
|
228
|
-
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
229
219
|
|
|
230
220
|
# Strategy 2 — request.data / request.POST pattern
|
|
231
221
|
try:
|
|
@@ -299,16 +289,10 @@ def extract_flask_schema(view_func, method):
|
|
|
299
289
|
"""
|
|
300
290
|
if method.upper() == "GET":
|
|
301
291
|
return None
|
|
302
|
-
|
|
303
|
-
print(f"\n[DEBUG] >>> extract_flask_schema: {method} handler={getattr(view_func, '__name__', '?')}")
|
|
304
|
-
print(f"[DEBUG] has __apidoc__: {hasattr(view_func, '__apidoc__')}")
|
|
305
|
-
print(f"[DEBUG] has _schema: {hasattr(view_func, '_schema')}")
|
|
306
|
-
print(f"[DEBUG] has view_class: {hasattr(view_func, 'view_class')}")
|
|
307
292
|
try:
|
|
308
293
|
hints = typing.get_type_hints(view_func) if callable(view_func) else {}
|
|
309
|
-
print(f"[DEBUG] type hints: {hints}")
|
|
310
294
|
except Exception:
|
|
311
|
-
|
|
295
|
+
pass
|
|
312
296
|
|
|
313
297
|
try:
|
|
314
298
|
# ── 1. Flask-RESTX / Flask-RESTPlus ──────────────────────────────
|
|
@@ -332,7 +316,6 @@ def extract_flask_schema(view_func, method):
|
|
|
332
316
|
result = {"type": "object", "properties": properties}
|
|
333
317
|
if required:
|
|
334
318
|
result["required"] = required
|
|
335
|
-
print(f"[BotVersion SDK] ✅ Flask-RESTX schema extracted: {list(properties.keys())}")
|
|
336
319
|
return result
|
|
337
320
|
|
|
338
321
|
# ── 2. Marshmallow schema ─────────────────────────────────────────
|
|
@@ -345,7 +328,6 @@ def extract_flask_schema(view_func, method):
|
|
|
345
328
|
if schema is not None:
|
|
346
329
|
marshmallow_result = _extract_marshmallow_schema(schema)
|
|
347
330
|
if marshmallow_result:
|
|
348
|
-
print(f"[BotVersion SDK] ✅ Marshmallow schema extracted from view: {list(marshmallow_result.get('properties', {}).keys())}")
|
|
349
331
|
return marshmallow_result
|
|
350
332
|
|
|
351
333
|
# ── 3. Flask-RESTX MethodView / Resource ─────────────────────────
|
|
@@ -362,7 +344,6 @@ def extract_flask_schema(view_func, method):
|
|
|
362
344
|
if schema:
|
|
363
345
|
marshmallow_result = _extract_marshmallow_schema(schema)
|
|
364
346
|
if marshmallow_result:
|
|
365
|
-
print(f"[BotVersion SDK] ✅ Marshmallow schema extracted from method: {list(marshmallow_result.get('properties', {}).keys())}")
|
|
366
347
|
return marshmallow_result
|
|
367
348
|
|
|
368
349
|
# Check for RESTX expect decorator
|
|
@@ -391,15 +372,13 @@ def extract_flask_schema(view_func, method):
|
|
|
391
372
|
pydantic_model = getattr(view_func, "_pydantic_model", None)
|
|
392
373
|
if pydantic_model and hasattr(pydantic_model, "model_json_schema"):
|
|
393
374
|
schema = pydantic_model.model_json_schema()
|
|
394
|
-
print(f"[BotVersion SDK] ✅ Pydantic schema extracted from Flask view")
|
|
395
375
|
return schema
|
|
396
376
|
if pydantic_model and hasattr(pydantic_model, "schema"):
|
|
397
377
|
schema = pydantic_model.schema()
|
|
398
|
-
print(f"[BotVersion SDK] ✅ Pydantic v1 schema extracted from Flask view")
|
|
399
378
|
return schema
|
|
400
379
|
|
|
401
|
-
except Exception
|
|
402
|
-
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
403
382
|
|
|
404
383
|
# Strategy 5 — plain request.json / request.get_json() / request.form pattern
|
|
405
384
|
try:
|
|
@@ -480,11 +459,10 @@ def extract_restx_resource_schema(app, rule, method):
|
|
|
480
459
|
result = {"type": "object", "properties": properties}
|
|
481
460
|
if required:
|
|
482
461
|
result["required"] = required
|
|
483
|
-
print(f"[BotVersion SDK] ✅ RESTX Resource schema: {list(properties.keys())}")
|
|
484
462
|
return result
|
|
485
463
|
|
|
486
|
-
except Exception
|
|
487
|
-
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
488
466
|
return None
|
|
489
467
|
|
|
490
468
|
|
|
@@ -531,7 +509,6 @@ def _extract_marshmallow_schema(schema):
|
|
|
531
509
|
except ImportError:
|
|
532
510
|
return None
|
|
533
511
|
except Exception as e:
|
|
534
|
-
print(f"[BotVersion SDK] ⚠ Marshmallow extraction failed: {e}")
|
|
535
512
|
return None
|
|
536
513
|
|
|
537
514
|
|
|
@@ -577,12 +554,10 @@ def _walk_django_patterns(patterns, prefix, endpoints, seen):
|
|
|
577
554
|
for pattern in patterns:
|
|
578
555
|
if isinstance(pattern, URLResolver):
|
|
579
556
|
sub_prefix = join_paths(prefix, _django_pattern_to_path(str(pattern.pattern)))
|
|
580
|
-
print(f"[BotVersion SDK] 📁 resolver: '{str(pattern.pattern)}' → prefix: '{sub_prefix}'")
|
|
581
557
|
_walk_django_patterns(pattern.url_patterns, sub_prefix, endpoints, seen)
|
|
582
558
|
|
|
583
559
|
elif isinstance(pattern, URLPattern):
|
|
584
560
|
path = join_paths(prefix, _django_pattern_to_path(str(pattern.pattern)))
|
|
585
|
-
print(f"[BotVersion SDK] 🔍 endpoint: '{str(pattern.pattern)}' → path: '{path}'")
|
|
586
561
|
methods = _detect_django_methods(pattern.callback)
|
|
587
562
|
handler_name = getattr(pattern.callback, "__name__", None)
|
|
588
563
|
|
|
@@ -679,6 +654,8 @@ def infer_field_type(field_name, source_code):
|
|
|
679
654
|
rf"int\s*\(\s*{field_name}\s*\)",
|
|
680
655
|
rf"float\s*\(\s*{field_name}\s*\)",
|
|
681
656
|
rf"isinstance\s*\(\s*{field_name}\s*,\s*(int|float)\s*\)",
|
|
657
|
+
rf"not\s+isinstance\s*\(\s*{field_name}\s*,\s*(int|float)\s*\)",
|
|
658
|
+
rf"type\s*\(\s*{field_name}\s*\)\s*is\s*(not\s+)?(int|float)",
|
|
682
659
|
]
|
|
683
660
|
if any(re.search(p, source_code) for p in number_patterns):
|
|
684
661
|
return "number"
|
|
@@ -689,6 +666,8 @@ def infer_field_type(field_name, source_code):
|
|
|
689
666
|
rf"(True|False)\s*==\s*{field_name}",
|
|
690
667
|
rf"isinstance\s*\(\s*{field_name}\s*,\s*bool\s*\)",
|
|
691
668
|
rf"bool\s*\(\s*{field_name}\s*\)",
|
|
669
|
+
rf"type\s*\(\s*{field_name}\s*\)\s*is\s*(not\s+)?bool",
|
|
670
|
+
rf"not\s+isinstance\s*\(\s*{field_name}\s*,\s*bool\s*\)",
|
|
692
671
|
]
|
|
693
672
|
if any(re.search(p, source_code) for p in bool_patterns):
|
|
694
673
|
return "boolean"
|
|
@@ -744,18 +723,14 @@ def extract_request_body_schema(route, method):
|
|
|
744
723
|
if method not in ("POST", "PUT", "PATCH"):
|
|
745
724
|
return None
|
|
746
725
|
|
|
747
|
-
print(f"\n[DEBUG] >>> extract_request_body_schema called: {method} {getattr(route, 'path', '?')}")
|
|
748
|
-
|
|
749
726
|
try:
|
|
750
727
|
# path param names — we must exclude these from body
|
|
751
728
|
path_param_names = set()
|
|
752
729
|
if hasattr(route, "dependant") and hasattr(route.dependant, "path_params"):
|
|
753
730
|
path_param_names = {f.name for f in route.dependant.path_params}
|
|
754
|
-
print(f"[DEBUG] path_param_names to exclude: {path_param_names}")
|
|
755
731
|
|
|
756
732
|
# Strategy 1: route.dependant.body_params
|
|
757
733
|
if hasattr(route, "dependant") and route.dependant.body_params:
|
|
758
|
-
print(f"[DEBUG] body_params found: {[f.name for f in route.dependant.body_params]}")
|
|
759
734
|
properties = {}
|
|
760
735
|
required = []
|
|
761
736
|
|
|
@@ -764,7 +739,6 @@ def extract_request_body_schema(route, method):
|
|
|
764
739
|
|
|
765
740
|
# Skip path params — they are NOT body fields
|
|
766
741
|
if field_name in path_param_names:
|
|
767
|
-
print(f"[DEBUG] Skipping path param: {field_name}")
|
|
768
742
|
continue
|
|
769
743
|
|
|
770
744
|
annotation = None
|
|
@@ -775,8 +749,6 @@ def extract_request_body_schema(route, method):
|
|
|
775
749
|
if annotation is None:
|
|
776
750
|
annotation = getattr(field, "type_", None)
|
|
777
751
|
|
|
778
|
-
print(f"[DEBUG] Field: {field_name}, annotation: {annotation}")
|
|
779
|
-
|
|
780
752
|
if annotation and hasattr(annotation, "model_json_schema"):
|
|
781
753
|
schema = annotation.model_json_schema()
|
|
782
754
|
properties.update(schema.get("properties", {}))
|
|
@@ -834,10 +806,8 @@ def extract_request_body_schema(route, method):
|
|
|
834
806
|
result["required"] = list(set(required))
|
|
835
807
|
return result
|
|
836
808
|
|
|
837
|
-
except Exception
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
print(f"[DEBUG] <<< returning None for {method} {getattr(route, 'path', '?')}")
|
|
809
|
+
except Exception:
|
|
810
|
+
pass
|
|
841
811
|
return None
|
|
842
812
|
|
|
843
813
|
|
|
@@ -932,7 +902,6 @@ def _walk_frontend_dir(directory, segments, patterns, seen):
|
|
|
932
902
|
continue # skip static routes with no dynamic params
|
|
933
903
|
|
|
934
904
|
patterns.append({"pattern": pattern, "params": param_map})
|
|
935
|
-
print(f"[BotVersion SDK] Found frontend route pattern: {pattern} → {param_map}")
|
|
936
905
|
|
|
937
906
|
|
|
938
907
|
|
|
@@ -992,8 +961,6 @@ def _scan_config_based_routes(cwd):
|
|
|
992
961
|
except OSError:
|
|
993
962
|
continue
|
|
994
963
|
|
|
995
|
-
print(f"[BotVersion SDK] Scanning config-based routes in: {file_path}")
|
|
996
|
-
|
|
997
964
|
# React Router JSX: <Route path="/:projectId/dashboard" />
|
|
998
965
|
for match in re.finditer(r'<Route[^>]+path=["\']([^"\']+)["\']', content):
|
|
999
966
|
_add_config_pattern(match.group(1), seen, patterns)
|
|
@@ -1029,5 +996,4 @@ def _add_config_pattern(route_path, seen, patterns):
|
|
|
1029
996
|
if not param_map:
|
|
1030
997
|
return
|
|
1031
998
|
|
|
1032
|
-
patterns.append({"pattern": normalized, "params": param_map})
|
|
1033
|
-
print(f"[BotVersion SDK] Found config-based route pattern: {normalized} → {param_map}")
|
|
999
|
+
patterns.append({"pattern": normalized, "params": param_map})
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="botversion-sdk",
|
|
5
|
-
version="1.0.
|
|
5
|
+
version="1.0.4",
|
|
6
6
|
description="BotVersion SDK — automatically discover and register your API endpoints",
|
|
7
7
|
long_description=open("README.md").read() if __import__("os").path.exists("README.md") else "",
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: botversion-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: BotVersion AI Agent SDK for FastAPI, Flask, and Django
|
|
5
5
|
Home-page: https://github.com/botversion/botversion-sdk-python
|
|
6
6
|
Author: BotVersion
|
|
7
7
|
Author-email: Saurav Dhakal <sauravdhakal828@gmail.com>
|
|
8
8
|
Requires-Python: >=3.7
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: websocket-client>=1.0.0
|
|
11
10
|
Dynamic: author
|
|
12
11
|
Dynamic: description-content-type
|
|
13
12
|
Dynamic: home-page
|
|
@@ -9,7 +9,6 @@ botversion_sdk.egg-info/PKG-INFO
|
|
|
9
9
|
botversion_sdk.egg-info/SOURCES.txt
|
|
10
10
|
botversion_sdk.egg-info/dependency_links.txt
|
|
11
11
|
botversion_sdk.egg-info/entry_points.txt
|
|
12
|
-
botversion_sdk.egg-info/requires.txt
|
|
13
12
|
botversion_sdk.egg-info/top_level.txt
|
|
14
13
|
botversion_sdk/cli/detector.py
|
|
15
14
|
botversion_sdk/cli/generator.py
|
|
@@ -4,13 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "botversion-sdk"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.5"
|
|
8
8
|
description = "BotVersion AI Agent SDK for FastAPI, Flask, and Django"
|
|
9
9
|
authors = [{name = "Saurav Dhakal", email = "sauravdhakal828@gmail.com"}]
|
|
10
10
|
requires-python = ">=3.8"
|
|
11
|
-
dependencies = [
|
|
12
|
-
"websocket-client>=1.0.0"
|
|
13
|
-
]
|
|
11
|
+
dependencies = []
|
|
14
12
|
|
|
15
13
|
[project.scripts]
|
|
16
14
|
botversion-init = "botversion_sdk.cli.init:main"
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="botversion-sdk",
|
|
5
|
-
version="1.0.
|
|
5
|
+
version="1.0.5",
|
|
6
6
|
description="BotVersion SDK — automatically discover and register your API endpoints",
|
|
7
7
|
long_description=open("README.md").read() if __import__("os").path.exists("README.md") else "",
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
websocket-client>=1.0.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|