botversion-sdk 1.0.5__tar.gz → 1.1.6__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 (21) hide show
  1. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/PKG-INFO +1 -1
  2. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/__init__.py +114 -7
  3. botversion_sdk-1.1.6/botversion_sdk/cli/__init__.py +0 -0
  4. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/client.py +16 -8
  5. botversion_sdk-1.1.6/botversion_sdk/interceptor.py +552 -0
  6. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/scanner.py +659 -32
  7. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk.egg-info/PKG-INFO +1 -1
  8. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk.egg-info/SOURCES.txt +1 -1
  9. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/pyproject.toml +1 -1
  10. {botversion_sdk-1.0.5/botversion_sdk → botversion_sdk-1.1.6}/setup.py +1 -1
  11. botversion_sdk-1.0.5/botversion_sdk/interceptor.py +0 -257
  12. botversion_sdk-1.0.5/setup.py +0 -42
  13. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/cli/detector.py +0 -0
  14. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/cli/generator.py +0 -0
  15. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/cli/init.py +0 -0
  16. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/cli/prompts.py +0 -0
  17. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk/cli/writer.py +0 -0
  18. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk.egg-info/dependency_links.txt +0 -0
  19. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk.egg-info/entry_points.txt +0 -0
  20. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/botversion_sdk.egg-info/top_level.txt +0 -0
  21. {botversion_sdk-1.0.5 → botversion_sdk-1.1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botversion-sdk
3
- Version: 1.0.5
3
+ Version: 1.1.6
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
@@ -10,6 +10,14 @@ from .interceptor import (
10
10
  attach_fastapi_interceptor,
11
11
  attach_flask_interceptor,
12
12
  attach_django_interceptor,
13
+ attach_starlette_interceptor,
14
+ attach_sanic_interceptor,
15
+ attach_falcon_interceptor,
16
+ attach_bottle_interceptor,
17
+ attach_aiohttp_interceptor,
18
+ attach_tornado_interceptor,
19
+ attach_pyramid_interceptor,
20
+ attach_cherrypy_interceptor,
13
21
  )
14
22
 
15
23
  _initialized = False
@@ -55,6 +63,23 @@ def init(app=None, api_key=None, **options):
55
63
  attach_flask_interceptor(app, _client, interceptor_options)
56
64
  elif framework == "django":
57
65
  attach_django_interceptor(_client, interceptor_options)
66
+ # ── NEW ───────────────────────────────────────────────────────
67
+ elif framework == "starlette":
68
+ attach_starlette_interceptor(app, _client, interceptor_options)
69
+ elif framework == "sanic":
70
+ attach_sanic_interceptor(app, _client, interceptor_options)
71
+ elif framework == "falcon":
72
+ attach_falcon_interceptor(app, _client, interceptor_options)
73
+ elif framework == "bottle":
74
+ attach_bottle_interceptor(app, _client, interceptor_options)
75
+ elif framework == "aiohttp":
76
+ attach_aiohttp_interceptor(app, _client, interceptor_options)
77
+ elif framework == "tornado":
78
+ attach_tornado_interceptor(app, _client, interceptor_options)
79
+ elif framework == "pyramid":
80
+ attach_pyramid_interceptor(app, _client, interceptor_options)
81
+ elif framework == "cherrypy":
82
+ attach_cherrypy_interceptor(app, _client, interceptor_options)
58
83
  return
59
84
 
60
85
  _initialized = True
@@ -96,6 +121,23 @@ def init(app=None, api_key=None, **options):
96
121
  attach_flask_interceptor(app, _client, interceptor_options)
97
122
  elif framework == "django":
98
123
  attach_django_interceptor(_client, interceptor_options)
124
+ # ── NEW ───────────────────────────────────────────────────────────────────
125
+ elif framework == "starlette":
126
+ attach_starlette_interceptor(app, _client, interceptor_options)
127
+ elif framework == "sanic":
128
+ attach_sanic_interceptor(app, _client, interceptor_options)
129
+ elif framework == "falcon":
130
+ attach_falcon_interceptor(app, _client, interceptor_options)
131
+ elif framework == "bottle":
132
+ attach_bottle_interceptor(app, _client, interceptor_options)
133
+ elif framework == "aiohttp":
134
+ attach_aiohttp_interceptor(app, _client, interceptor_options)
135
+ elif framework == "tornado":
136
+ attach_tornado_interceptor(app, _client, interceptor_options)
137
+ elif framework == "pyramid":
138
+ attach_pyramid_interceptor(app, _client, interceptor_options)
139
+ elif framework == "cherrypy":
140
+ attach_cherrypy_interceptor(app, _client, interceptor_options)
99
141
  else:
100
142
  return
101
143
 
@@ -110,8 +152,8 @@ def init(app=None, api_key=None, **options):
110
152
  elif framework == "django":
111
153
  endpoints = scan_routes(None, "django")
112
154
 
113
- else:
114
- return
155
+ # For any other framework with no app object, skip backend scan
156
+ # but still continue to frontend scan below
115
157
 
116
158
  if endpoints:
117
159
  _client.register_endpoints_now(endpoints)
@@ -127,31 +169,57 @@ def init(app=None, api_key=None, **options):
127
169
  _client.register_route_patterns(route_patterns)
128
170
 
129
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
+
130
178
  @app.after_request
131
179
  def _botversion_first_scan(response):
132
180
  if not getattr(app, '_botversion_scanned', False):
133
181
  app._botversion_scanned = True
134
- threading.Thread(target=_run_scan, daemon=True).start()
182
+ t = threading.Thread(target=_run_scan, daemon=False)
183
+ t.start()
184
+ t.join(timeout=15)
135
185
  return response
136
186
 
137
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
+
138
196
  @app.middleware("http")
139
197
  async def _botversion_first_scan(request, call_next):
140
198
  if not getattr(app, '_botversion_scanned', False):
141
199
  app._botversion_scanned = True
142
- threading.Thread(target=_run_scan, daemon=True).start()
200
+ t = threading.Thread(target=_run_scan, daemon=False)
201
+ t.start()
202
+ t.join(timeout=15)
143
203
  return await call_next(request)
144
204
 
145
205
  elif framework == "django":
146
206
  try:
147
207
  from django.apps import apps
148
208
  if apps.ready:
149
- threading.Thread(target=_run_scan, daemon=True).start()
209
+ t = threading.Thread(target=_run_scan, daemon=False)
210
+ t.start()
211
+ t.join(timeout=15)
150
212
  except Exception:
151
- t = threading.Timer(2.0, _run_scan)
152
- t.daemon = True
213
+ t = threading.Timer(3.0, _run_scan)
214
+ t.daemon = False
153
215
  t.start()
154
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
+
155
223
 
156
224
  def get_endpoints():
157
225
  """Get all registered endpoints for this workspace."""
@@ -172,11 +240,32 @@ def register_endpoint(endpoint):
172
240
  def _detect_framework(app):
173
241
  if app is not None:
174
242
  app_type = type(app).__module__ + "." + type(app).__name__
243
+
244
+ # ── Existing ──────────────────────────────────────────────────────
175
245
  if "fastapi" in app_type.lower():
176
246
  return "fastapi"
177
247
  if "flask" in app_type.lower():
178
248
  return "flask"
179
249
 
250
+ # ── NEW ───────────────────────────────────────────────────────────
251
+ if "starlette" in app_type.lower():
252
+ return "starlette"
253
+ if "sanic" in app_type.lower():
254
+ return "sanic"
255
+ if "falcon" in app_type.lower():
256
+ return "falcon"
257
+ if "bottle" in app_type.lower():
258
+ return "bottle"
259
+ if "aiohttp" in app_type.lower():
260
+ return "aiohttp"
261
+ if "tornado" in app_type.lower():
262
+ return "tornado"
263
+ if "pyramid" in app_type.lower():
264
+ return "pyramid"
265
+ if "cherrypy" in app_type.lower():
266
+ return "cherrypy"
267
+
268
+ # ── No app object passed — try detecting from imported modules ────────
180
269
  if app is None:
181
270
  if "django" in sys.modules:
182
271
  try:
@@ -186,11 +275,29 @@ def _detect_framework(app):
186
275
  except Exception:
187
276
  pass
188
277
 
278
+ # ── Fallback — check sys.modules ──────────────────────────────────────
189
279
  if "fastapi" in sys.modules:
190
280
  return "fastapi"
191
281
  if "flask" in sys.modules:
192
282
  return "flask"
193
283
  if "django" in sys.modules:
194
284
  return "django"
285
+ # ── NEW fallbacks ─────────────────────────────────────────────────────
286
+ if "starlette" in sys.modules:
287
+ return "starlette"
288
+ if "sanic" in sys.modules:
289
+ return "sanic"
290
+ if "falcon" in sys.modules:
291
+ return "falcon"
292
+ if "bottle" in sys.modules:
293
+ return "bottle"
294
+ if "aiohttp" in sys.modules:
295
+ return "aiohttp"
296
+ if "tornado" in sys.modules:
297
+ return "tornado"
298
+ if "pyramid" in sys.modules:
299
+ return "pyramid"
300
+ if "cherrypy" in sys.modules:
301
+ return "cherrypy"
195
302
 
196
303
  return None
File without changes
@@ -47,8 +47,9 @@ class BotVersionClient:
47
47
  "endpoints": endpoints,
48
48
  })
49
49
  return data
50
- except Exception:
51
- pass
50
+ except Exception as e:
51
+ if self.debug:
52
+ print(f"[botversion] register_endpoints_now failed: {e}")
52
53
 
53
54
  # ── Flush batch ──────────────────────────────────────────────────────────
54
55
 
@@ -65,8 +66,9 @@ class BotVersionClient:
65
66
  "workspaceKey": self.api_key,
66
67
  "endpoints": to_send,
67
68
  })
68
- except Exception:
69
- pass
69
+ except Exception as e:
70
+ if self.debug:
71
+ print(f"[botversion] flush failed: {e}")
70
72
 
71
73
  # ── Update single endpoint (runtime interceptor) ─────────────────────────
72
74
 
@@ -80,8 +82,9 @@ class BotVersionClient:
80
82
  "responseBody": endpoint.get("response_body"),
81
83
  "detectedBy": endpoint.get("detected_by", "runtime"),
82
84
  })
83
- except Exception:
84
- pass
85
+ except Exception as e:
86
+ if self.debug:
87
+ print(f"[botversion] update_endpoint failed: {e}")
85
88
 
86
89
 
87
90
  # ── Register frontend route patterns ─────────────────────────────────────────
@@ -94,8 +97,9 @@ class BotVersionClient:
94
97
  "workspaceKey": self.api_key,
95
98
  "patterns": patterns,
96
99
  })
97
- except Exception:
98
- pass
100
+ except Exception as e:
101
+ if self.debug:
102
+ print(f"[botversion] register_route_patterns failed: {e}")
99
103
 
100
104
  # ── Get all endpoints ────────────────────────────────────────────────────
101
105
 
@@ -123,6 +127,8 @@ class BotVersionClient:
123
127
  try:
124
128
  with urllib.request.urlopen(req, timeout=self.timeout) as res:
125
129
  response_data = res.read().decode("utf-8")
130
+ if not response_data or not response_data.strip():
131
+ return {}
126
132
  return json.loads(response_data)
127
133
  except urllib.error.HTTPError as e:
128
134
  error_body = e.read().decode("utf-8")
@@ -149,6 +155,8 @@ class BotVersionClient:
149
155
  try:
150
156
  with urllib.request.urlopen(req, timeout=self.timeout) as res:
151
157
  response_data = res.read().decode("utf-8")
158
+ if not response_data or not response_data.strip():
159
+ return {}
152
160
  return json.loads(response_data)
153
161
  except urllib.error.HTTPError as e:
154
162
  error_body = e.read().decode("utf-8")