botversion-sdk 1.0.1__tar.gz → 1.0.3__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.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: botversion-sdk
3
+ Version: 1.0.3
4
+ Summary: BotVersion AI Agent SDK for FastAPI, Flask, and Django
5
+ Home-page: https://github.com/botversion/botversion-sdk-python
6
+ Author: BotVersion
7
+ Author-email: Saurav Dhakal <sauravdhakal828@gmail.com>
8
+ Requires-Python: >=3.7
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: websocket-client>=1.0.0
11
+ Dynamic: author
12
+ Dynamic: description-content-type
13
+ Dynamic: home-page
14
+ Dynamic: requires-python
@@ -0,0 +1,226 @@
1
+ # botversion-sdk-python/botversion-sdk/__init__.py
2
+ import sys
3
+ import threading
4
+ import builtins
5
+ import os
6
+
7
+ from .client import BotVersionClient
8
+ from .scanner import scan_routes, scan_frontend_routes
9
+ from .interceptor import (
10
+ attach_fastapi_interceptor,
11
+ attach_flask_interceptor,
12
+ attach_django_interceptor,
13
+ )
14
+
15
+ _initialized = False
16
+ _client = None
17
+ _options = {}
18
+ _app = None
19
+
20
+
21
+ def init(app=None, api_key=None, **options):
22
+ """
23
+ Initialize the BotVersion SDK.
24
+
25
+ Works for FastAPI, Flask, and Django — auto-detects the framework.
26
+
27
+ Usage:
28
+ # FastAPI
29
+ botversion_sdk.init(app, api_key="YOUR_KEY")
30
+
31
+ # Flask
32
+ botversion_sdk.init(app, api_key="YOUR_KEY")
33
+
34
+ # Django — no app object needed:
35
+ botversion_sdk.init(api_key="YOUR_KEY")
36
+ """
37
+ global _initialized, _client, _options, _app
38
+
39
+ if not api_key:
40
+ print("[BotVersion SDK] ❌ api_key is required.")
41
+ return
42
+
43
+ # Restore from builtins if module was re-imported after hot reload
44
+ if getattr(builtins, "_botversion_client", None):
45
+ _client = builtins._botversion_client
46
+ _options = builtins._botversion_options
47
+ _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")
53
+ return
54
+
55
+ _initialized = True
56
+ _options = dict(options)
57
+ _options["api_key"] = api_key
58
+ _app = app
59
+
60
+ debug = options.get("debug", False)
61
+
62
+ # ── Auto-detect framework ─────────────────────────────────────────────────
63
+ framework = _detect_framework(app)
64
+
65
+ if not framework:
66
+ print("[BotVersion SDK] ❌ Could not detect framework.")
67
+ print("[BotVersion SDK] ❌ Make sure FastAPI, Flask, or Django is installed.")
68
+ _initialized = False
69
+ return
70
+
71
+ _client = BotVersionClient({
72
+ "api_key": api_key,
73
+ "platform_url": options.get("platform_url", "http://localhost:3000"),
74
+ "debug": debug,
75
+ "timeout": options.get("timeout", 30),
76
+ "flush_delay": options.get("flush_delay", 3),
77
+ })
78
+
79
+ # Store globally so hot-reload can restore state
80
+ builtins._botversion_client = _client
81
+ builtins._botversion_options = _options
82
+
83
+ if debug:
84
+ print(f"[BotVersion SDK] ✅ Framework detected: {framework}")
85
+
86
+ interceptor_options = {
87
+ "exclude": options.get("exclude", []),
88
+ "api_prefix": options.get("api_prefix", None),
89
+ "debug": debug,
90
+ }
91
+
92
+ # ── Attach runtime interceptor ───────────────────────────────────────────
93
+ if framework == "fastapi":
94
+ attach_fastapi_interceptor(app, _client, interceptor_options)
95
+ elif framework == "flask":
96
+ attach_flask_interceptor(app, _client, interceptor_options)
97
+ elif framework == "django":
98
+ attach_django_interceptor(_client, interceptor_options)
99
+ else:
100
+ print(f"[BotVersion SDK] ❌ Unsupported framework: {framework}")
101
+ return
102
+
103
+ if debug:
104
+ print("[BotVersion SDK] ✅ Runtime interceptor attached")
105
+
106
+ # ── Static scan (delayed 500ms — let app finish registering routes) ──────
107
+ def _run_scan():
108
+ try:
109
+ endpoints = []
110
+
111
+ if app is not None:
112
+ print(f"[BotVersion SDK] Scanning {framework} routes...")
113
+ 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
+
124
+ elif framework == "django":
125
+ print("[BotVersion SDK] Scanning Django routes...")
126
+ endpoints = scan_routes(None, "django")
127
+ print(f"[BotVersion SDK] Found {len(endpoints)} Django routes")
128
+
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
+ else:
133
+ print("[BotVersion SDK] ❌ No routes to scan.")
134
+ return
135
+
136
+ if endpoints:
137
+ print(f"[BotVersion SDK] Sending {len(endpoints)} endpoints to platform...")
138
+ _client.register_endpoints_now(endpoints)
139
+ print(f"[BotVersion SDK] ✅ Static scan complete — {len(endpoints)} endpoints registered")
140
+
141
+ except Exception as e:
142
+ print(f"[BotVersion SDK] ❌ Scan error: {e}")
143
+ if debug:
144
+ import traceback
145
+ traceback.print_exc()
146
+
147
+ cwd = options.get("cwd", os.getcwd())
148
+ route_patterns = scan_frontend_routes(cwd)
149
+ if route_patterns:
150
+ print(f"[BotVersion SDK] Found {len(route_patterns)} frontend route patterns")
151
+ _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
+
159
+ if framework == "flask":
160
+ @app.after_request
161
+ def _botversion_first_scan(response):
162
+ if not getattr(app, '_botversion_scanned', False):
163
+ app._botversion_scanned = True
164
+ threading.Thread(target=_run_scan, daemon=True).start()
165
+ return response
166
+
167
+ elif framework == "fastapi":
168
+ @app.middleware("http")
169
+ async def _botversion_first_scan(request, call_next):
170
+ if not getattr(app, '_botversion_scanned', False):
171
+ app._botversion_scanned = True
172
+ threading.Thread(target=_run_scan, daemon=True).start()
173
+ return await call_next(request)
174
+
175
+ elif framework == "django":
176
+ try:
177
+ from django.apps import apps
178
+ if apps.ready:
179
+ threading.Thread(target=_run_scan, daemon=True).start()
180
+ except Exception:
181
+ t = threading.Timer(2.0, _run_scan)
182
+ t.daemon = True
183
+ t.start()
184
+
185
+
186
+ def get_endpoints():
187
+ """Get all registered endpoints for this workspace."""
188
+ if not _client:
189
+ raise RuntimeError("BotVersion SDK not initialized. Call botversion_sdk.init() first.")
190
+ return _client.get_endpoints()
191
+
192
+
193
+ def register_endpoint(endpoint):
194
+ """Manually register a single endpoint."""
195
+ if not _client:
196
+ raise RuntimeError("BotVersion SDK not initialized.")
197
+ return _client.register_endpoints([endpoint])
198
+
199
+
200
+ # ── Framework auto-detection ─────────────────────────────────────────────────
201
+
202
+ def _detect_framework(app):
203
+ if app is not None:
204
+ app_type = type(app).__module__ + "." + type(app).__name__
205
+ if "fastapi" in app_type.lower():
206
+ return "fastapi"
207
+ if "flask" in app_type.lower():
208
+ return "flask"
209
+
210
+ if app is None:
211
+ if "django" in sys.modules:
212
+ try:
213
+ from django.conf import settings
214
+ if settings.configured:
215
+ return "django"
216
+ except Exception:
217
+ pass
218
+
219
+ if "fastapi" in sys.modules:
220
+ return "fastapi"
221
+ if "flask" in sys.modules:
222
+ return "flask"
223
+ if "django" in sys.modules:
224
+ return "django"
225
+
226
+ return None