botversion-sdk 1.0.3__tar.gz → 1.0.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.
Files changed (20) hide show
  1. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/PKG-INFO +1 -2
  2. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/__init__.py +15 -45
  3. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/cli/detector.py +2 -1
  4. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/cli/generator.py +7 -7
  5. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/cli/init.py +2 -2
  6. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/cli/writer.py +1 -1
  7. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/client.py +11 -148
  8. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/interceptor.py +9 -148
  9. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/scanner.py +20 -54
  10. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/setup.py +1 -1
  11. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk.egg-info/PKG-INFO +1 -2
  12. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk.egg-info/SOURCES.txt +0 -1
  13. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/pyproject.toml +2 -4
  14. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/setup.py +1 -1
  15. botversion_sdk-1.0.3/botversion_sdk.egg-info/requires.txt +0 -1
  16. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk/cli/prompts.py +0 -0
  17. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk.egg-info/dependency_links.txt +0 -0
  18. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk.egg-info/entry_points.txt +0 -0
  19. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/botversion_sdk.egg-info/top_level.txt +0 -0
  20. {botversion_sdk-1.0.3 → botversion_sdk-1.0.4}/setup.cfg +0 -0
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botversion-sdk
3
- Version: 1.0.3
3
+ Version: 1.0.4
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
- print("[BotVersion SDK] Restored from builtins — skipping re-init")
49
- return
50
-
51
- if _initialized:
52
- print("[BotVersion SDK] ⚠ Already initialized — skipping")
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", "http://localhost:3000"),
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 in ('wsgi.py', 'asgi.py'): score += 5
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://app.botversion.com"),
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://app.botversion.com"),
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://app.botversion.com"),
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://docs.botversion.com/tornado
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://docs.botversion.com/aiohttp
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://docs.botversion.com/sanic
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://docs.botversion.com for manual setup instructions.
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: http://localhost:3000/settings")
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"http://localhost:3000/api/sdk/project-info?workspaceKey={args.key}"
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://docs.botversion.com")
552
+ lines.append(" Docs: https://botversion.com/docs")
553
553
  lines.append("")
554
554
 
555
555
  return "\n".join(lines)
@@ -6,20 +6,17 @@ 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", "http://localhost:3000")
14
+ platform_url = options.get("platform_url", "https://botversion.com")
18
15
 
19
16
  # Force IPv4 — on Windows, localhost resolves to ::1 (IPv6) in browsers
20
17
  # 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")
18
+ platform_url = platform_url.replace("https://botversion.com", "http://127.0.0.1")
19
+ platform_url = platform_url.replace("https://botversion.com", "https://127.0.0.1")
23
20
 
24
21
  self.platform_url = platform_url
25
22
  self.debug = options.get("debug", False)
@@ -32,133 +29,12 @@ class BotVersionClient:
32
29
  self._lock = threading.Lock()
33
30
  atexit.register(self._flush)
34
31
 
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
32
  # ── Register endpoints (batched) ─────────────────────────────────────────
154
33
 
155
34
  def register_endpoints(self, endpoints):
156
35
  if not endpoints:
157
36
  return
158
37
 
159
- if self.debug:
160
- print(f"[BotVersion SDK] Queuing {len(endpoints)} endpoints for registration")
161
-
162
38
  with self._lock:
163
39
  self._queue.extend(endpoints)
164
40
 
@@ -175,11 +51,9 @@ class BotVersionClient:
175
51
  "workspaceKey": self.api_key,
176
52
  "endpoints": endpoints,
177
53
  })
178
- if self.debug:
179
- print(f"[BotVersion SDK] ✅ Registered {len(endpoints)} endpoints")
180
54
  return data
181
- except Exception as e:
182
- print(f"[BotVersion SDK] ⚠ Failed to register endpoints: {e}")
55
+ except Exception:
56
+ pass
183
57
 
184
58
  # ── Flush batch ──────────────────────────────────────────────────────────
185
59
 
@@ -191,20 +65,13 @@ class BotVersionClient:
191
65
  to_send = self._queue[:]
192
66
  self._queue = []
193
67
 
194
- if self.debug:
195
- print(f"[BotVersion SDK] Flushing {len(to_send)} endpoints to platform")
196
-
197
68
  try:
198
69
  data = self._post("/api/sdk/register-endpoints", {
199
70
  "workspaceKey": self.api_key,
200
71
  "endpoints": to_send,
201
72
  })
202
- if self.debug:
203
- succeeded = data.get("succeeded", len(to_send))
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}")
73
+ except Exception:
74
+ pass
208
75
 
209
76
  # ── Update single endpoint (runtime interceptor) ─────────────────────────
210
77
 
@@ -218,9 +85,8 @@ class BotVersionClient:
218
85
  "responseBody": endpoint.get("response_body"),
219
86
  "detectedBy": endpoint.get("detected_by", "runtime"),
220
87
  })
221
- except Exception as e:
222
- if self.debug:
223
- print(f"[BotVersion SDK] ⚠ Failed to update endpoint: {e}")
88
+ except Exception:
89
+ pass
224
90
 
225
91
 
226
92
  # ── Register frontend route patterns ─────────────────────────────────────────
@@ -233,11 +99,8 @@ class BotVersionClient:
233
99
  "workspaceKey": self.api_key,
234
100
  "patterns": patterns,
235
101
  })
236
- if self.debug:
237
- print(f"[BotVersion SDK] ✅ Registered {len(patterns)} route patterns")
238
- except Exception as e:
239
- if self.debug:
240
- print(f"[BotVersion SDK] ⚠ Failed to register route patterns: {e}")
102
+ except Exception:
103
+ pass
241
104
 
242
105
  # ── Get all endpoints ────────────────────────────────────────────────────
243
106
 
@@ -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
- if options.get("debug"):
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
- report_endpoint(client, method, path, body_structure, options)
163
+ if response.status_code < 500:
164
+ report_endpoint(client, method, path, body_structure, options)
283
165
 
284
- return await call_next(request)
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
- print("[BotVersion SDK] ❌ starlette not found — cannot attach FastAPI middleware")
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
- print("[BotVersion SDK] ❌ Flask not found — cannot attach interceptor")
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
- print("[BotVersion SDK] ❌ Django not found — cannot attach middleware")
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 as e:
68
- print(f"[BotVersion SDK] ⚠ FastAPI scan error: {e}")
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 as e:
123
- import traceback
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 as e:
155
- print(f"[BotVersion SDK] ⚠ Django scan error: {e}")
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 as e:
228
- print(f"[BotVersion SDK] ⚠ DRF schema extraction failed: {e}")
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
- print(f"[DEBUG] type hints: could not resolve")
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 as e:
402
- print(f"[BotVersion SDK] ⚠ Flask schema extraction failed: {e}")
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 as e:
487
- print(f"[BotVersion SDK] ⚠ RESTX Resource schema failed: {e}")
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 as e:
838
- print(f"[DEBUG] EXCEPTION: {e}")
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.3",
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
3
+ Version: 1.0.4
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.3"
7
+ version = "1.0.4"
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.3",
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 +0,0 @@
1
- websocket-client>=1.0.0
File without changes