botversion-sdk 1.1.6__tar.gz → 2.0.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botversion-sdk
3
- Version: 1.1.6
3
+ Version: 2.0.0
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
@@ -5,7 +5,7 @@ import builtins
5
5
  import os
6
6
 
7
7
  from .client import BotVersionClient
8
- from .scanner import scan_routes, scan_frontend_routes
8
+ from .scanner import scan_routes
9
9
  from .interceptor import (
10
10
  attach_fastapi_interceptor,
11
11
  attach_flask_interceptor,
@@ -20,12 +20,64 @@ from .interceptor import (
20
20
  attach_cherrypy_interceptor,
21
21
  )
22
22
 
23
+ def _classify_installation(framework):
24
+ """
25
+ Python/PHP SDKs only ever run on a backend server — there is no such
26
+ thing as a Python or PHP frontend project. So unlike the JS SDK, this
27
+ is a simple two-way classification: either a supported backend
28
+ framework was detected, or it wasn't.
29
+ """
30
+ if framework:
31
+ return "backend-only"
32
+ return "unknown"
33
+
23
34
  _initialized = False
24
35
  _client = None
25
36
  _options = {}
26
37
  _app = None
27
38
 
28
39
 
40
+ def _run_full_scan(app, framework, cwd, client, debug=False):
41
+ """
42
+ Runs a full scan (backend endpoints + frontend routes) and reports
43
+ results to the platform. This used to run automatically on server
44
+ start / first request. Now it only runs when the BotVersion dashboard's
45
+ "Scan" button sends a request to this SDK's scan-trigger endpoint
46
+ (see interceptor.py). This fixes scans not happening on redeploys
47
+ (e.g. Vercel/serverless) since there's no "restart" event to hook into.
48
+ """
49
+ result = {
50
+ "endpoint_count": 0,
51
+ "route_count": 0,
52
+ "project_type": None,
53
+ "detected_backend": framework,
54
+ "detected_frontend": None,
55
+ "sdk_language": "python",
56
+ }
57
+
58
+ try:
59
+ endpoints = []
60
+ if app is not None:
61
+ endpoints = scan_routes(app, framework)
62
+ elif framework == "django":
63
+ endpoints = scan_routes(None, "django")
64
+
65
+ result["endpoint_count"] = len(endpoints)
66
+
67
+ if endpoints:
68
+ client.register_endpoints_now(endpoints)
69
+
70
+ except Exception:
71
+ if debug:
72
+ import traceback
73
+ traceback.print_exc()
74
+
75
+ result["detected_frontend"] = None
76
+ result["project_type"] = _classify_installation(framework)
77
+
78
+ return result
79
+
80
+
29
81
  def init(app=None, api_key=None, **options):
30
82
  """
31
83
  Initialize the BotVersion SDK.
@@ -52,10 +104,16 @@ def init(app=None, api_key=None, **options):
52
104
  # Re-attach interceptor after hot reload
53
105
  framework = _detect_framework(app)
54
106
  if framework and _client:
107
+ _cwd = _options.get("cwd", os.getcwd())
108
+ _debug = _options.get("debug", False)
55
109
  interceptor_options = {
56
110
  "exclude": _options.get("exclude", []),
57
111
  "api_prefix": _options.get("api_prefix", None),
58
- "debug": _options.get("debug", False),
112
+ "debug": _debug,
113
+ "scan_secret": _options.get("api_key"),
114
+ "on_scan_requested": lambda: _run_full_scan(
115
+ app, framework, _cwd, _client, _debug
116
+ ),
59
117
  }
60
118
  if framework == "fastapi":
61
119
  attach_fastapi_interceptor(app, _client, interceptor_options)
@@ -94,6 +152,8 @@ def init(app=None, api_key=None, **options):
94
152
 
95
153
  if not framework:
96
154
  _initialized = False
155
+ if debug:
156
+ print("[botversion] No supported framework detected. SDK not initialized.")
97
157
  return
98
158
 
99
159
  _client = BotVersionClient({
@@ -108,10 +168,16 @@ def init(app=None, api_key=None, **options):
108
168
  builtins._botversion_client = _client
109
169
  builtins._botversion_options = _options
110
170
 
171
+ cwd = options.get("cwd", os.getcwd())
172
+
111
173
  interceptor_options = {
112
174
  "exclude": options.get("exclude", []),
113
175
  "api_prefix": options.get("api_prefix", None),
114
176
  "debug": debug,
177
+ "scan_secret": api_key,
178
+ "on_scan_requested": lambda: _run_full_scan(
179
+ app, framework, cwd, _client, debug
180
+ ),
115
181
  }
116
182
 
117
183
  # ── Attach runtime interceptor ───────────────────────────────────────────
@@ -141,85 +207,6 @@ def init(app=None, api_key=None, **options):
141
207
  else:
142
208
  return
143
209
 
144
- # ── Static scan (delayed 500ms — let app finish registering routes) ──────
145
- def _run_scan():
146
- try:
147
- endpoints = []
148
-
149
- if app is not None:
150
- endpoints = scan_routes(app, framework)
151
-
152
- elif framework == "django":
153
- endpoints = scan_routes(None, "django")
154
-
155
- # For any other framework with no app object, skip backend scan
156
- # but still continue to frontend scan below
157
-
158
- if endpoints:
159
- _client.register_endpoints_now(endpoints)
160
-
161
- except Exception as e:
162
- if debug:
163
- import traceback
164
- traceback.print_exc()
165
-
166
- cwd = options.get("cwd", os.getcwd())
167
- route_patterns = scan_frontend_routes(cwd)
168
- if route_patterns:
169
- _client.register_route_patterns(route_patterns)
170
-
171
- if framework == "flask":
172
- with app.app_context():
173
- t = threading.Thread(target=_run_scan, daemon=False)
174
- t.start()
175
- t.join(timeout=15)
176
- app._botversion_scanned = True
177
-
178
- @app.after_request
179
- def _botversion_first_scan(response):
180
- if not getattr(app, '_botversion_scanned', False):
181
- app._botversion_scanned = True
182
- t = threading.Thread(target=_run_scan, daemon=False)
183
- t.start()
184
- t.join(timeout=15)
185
- return response
186
-
187
- elif framework == "fastapi":
188
- @app.on_event("startup")
189
- async def _botversion_startup_scan():
190
- if not getattr(app, '_botversion_scanned', False):
191
- app._botversion_scanned = True
192
- t = threading.Thread(target=_run_scan, daemon=False)
193
- t.start()
194
- t.join(timeout=15)
195
-
196
- @app.middleware("http")
197
- async def _botversion_first_scan(request, call_next):
198
- if not getattr(app, '_botversion_scanned', False):
199
- app._botversion_scanned = True
200
- t = threading.Thread(target=_run_scan, daemon=False)
201
- t.start()
202
- t.join(timeout=15)
203
- return await call_next(request)
204
-
205
- elif framework == "django":
206
- try:
207
- from django.apps import apps
208
- if apps.ready:
209
- t = threading.Thread(target=_run_scan, daemon=False)
210
- t.start()
211
- t.join(timeout=15)
212
- except Exception:
213
- t = threading.Timer(3.0, _run_scan)
214
- t.daemon = False
215
- t.start()
216
-
217
- elif framework in ("starlette", "sanic", "falcon", "bottle",
218
- "aiohttp", "tornado", "pyramid", "cherrypy"):
219
- t = threading.Timer(3.0, _run_scan)
220
- t.daemon = False
221
- t.start()
222
-
223
210
 
224
211
  def get_endpoints():
225
212
  """Get all registered endpoints for this workspace."""
@@ -89,13 +89,14 @@ class BotVersionClient:
89
89
 
90
90
  # ── Register frontend route patterns ─────────────────────────────────────────
91
91
 
92
- def register_route_patterns(self, patterns):
93
- if not patterns:
92
+ def register_route_patterns(self, patterns, classification=None):
93
+ if not patterns and not classification:
94
94
  return
95
95
  try:
96
96
  self._post("/api/sdk/register-route-patterns", {
97
97
  "workspaceKey": self.api_key,
98
- "patterns": patterns,
98
+ "patterns": patterns or [],
99
+ "classification": classification,
99
100
  })
100
101
  except Exception as e:
101
102
  if self.debug:
@@ -121,6 +122,7 @@ class BotVersionClient:
121
122
  headers={
122
123
  "Content-Type": "application/json",
123
124
  "Content-Length": str(len(body)),
125
+ "X-BotVersion-SDK": "1.0.0-python",
124
126
  },
125
127
  )
126
128
 
@@ -150,6 +152,9 @@ class BotVersionClient:
150
152
  req = urllib.request.Request(
151
153
  url,
152
154
  method="GET",
155
+ headers={
156
+ "X-BotVersion-SDK": "1.0.0-python",
157
+ },
153
158
  )
154
159
 
155
160
  try: