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.
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/PKG-INFO +1 -1
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/botversion_sdk/__init__.py +68 -81
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/botversion_sdk/client.py +8 -3
- botversion_sdk-2.0.0/botversion_sdk/interceptor.py +923 -0
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/botversion_sdk/scanner.py +1 -327
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/botversion_sdk.egg-info/PKG-INFO +1 -1
- botversion_sdk-2.0.0/botversion_sdk.egg-info/SOURCES.txt +10 -0
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/pyproject.toml +1 -4
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/setup.py +1 -9
- botversion_sdk-1.1.6/botversion_sdk/cli/__init__.py +0 -0
- botversion_sdk-1.1.6/botversion_sdk/cli/detector.py +0 -1157
- botversion_sdk-1.1.6/botversion_sdk/cli/generator.py +0 -169
- botversion_sdk-1.1.6/botversion_sdk/cli/init.py +0 -673
- botversion_sdk-1.1.6/botversion_sdk/cli/prompts.py +0 -155
- botversion_sdk-1.1.6/botversion_sdk/cli/writer.py +0 -724
- botversion_sdk-1.1.6/botversion_sdk/interceptor.py +0 -552
- botversion_sdk-1.1.6/botversion_sdk.egg-info/SOURCES.txt +0 -17
- botversion_sdk-1.1.6/botversion_sdk.egg-info/entry_points.txt +0 -2
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/botversion_sdk.egg-info/dependency_links.txt +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/botversion_sdk.egg-info/top_level.txt +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-2.0.0}/setup.cfg +0 -0
|
@@ -5,7 +5,7 @@ import builtins
|
|
|
5
5
|
import os
|
|
6
6
|
|
|
7
7
|
from .client import BotVersionClient
|
|
8
|
-
from .scanner import scan_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":
|
|
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:
|