botversion-sdk 1.1.6__tar.gz → 1.1.7__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-1.1.7}/PKG-INFO +1 -1
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/interceptor.py +130 -3
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/PKG-INFO +1 -1
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/pyproject.toml +1 -1
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/setup.py +1 -1
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/__init__.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/__init__.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/detector.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/generator.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/init.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/prompts.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/writer.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/client.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/scanner.py +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/SOURCES.txt +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/dependency_links.txt +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/entry_points.txt +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/top_level.txt +0 -0
- {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/setup.cfg +0 -0
|
@@ -78,6 +78,19 @@ def build_body_structure(body):
|
|
|
78
78
|
structure[key] = "array"
|
|
79
79
|
elif val is None:
|
|
80
80
|
structure[key] = "null"
|
|
81
|
+
elif isinstance(val, dict):
|
|
82
|
+
# Capture one level of nested properties so agent can reconstruct
|
|
83
|
+
# the object shape — e.g. group: { teamId: null, parentId: null }
|
|
84
|
+
nested_props = {}
|
|
85
|
+
for nk, nv in val.items():
|
|
86
|
+
if nv is None:
|
|
87
|
+
nested_props[nk] = {"type": "string"}
|
|
88
|
+
else:
|
|
89
|
+
nested_props[nk] = {"type": type(nv).__name__}
|
|
90
|
+
if nested_props:
|
|
91
|
+
structure[key] = {"type": "object", "properties": nested_props}
|
|
92
|
+
else:
|
|
93
|
+
structure[key] = "object"
|
|
81
94
|
else:
|
|
82
95
|
structure[key] = type(val).__name__
|
|
83
96
|
|
|
@@ -87,20 +100,76 @@ def build_body_structure(body):
|
|
|
87
100
|
def body_structure_to_json_schema(body_structure):
|
|
88
101
|
"""
|
|
89
102
|
Convert body structure dict to JSON Schema format.
|
|
103
|
+
Handles both simple types (strings) and nested object descriptors.
|
|
90
104
|
"""
|
|
91
105
|
if not body_structure:
|
|
92
106
|
return None
|
|
93
107
|
|
|
94
108
|
properties = {}
|
|
95
|
-
for key,
|
|
96
|
-
|
|
109
|
+
for key, type_or_obj in body_structure.items():
|
|
110
|
+
# Nested object captured with properties
|
|
111
|
+
if isinstance(type_or_obj, dict) and "type" in type_or_obj:
|
|
112
|
+
properties[key] = type_or_obj
|
|
113
|
+
# Simple type string
|
|
114
|
+
elif type_or_obj in ("[redacted]", "null"):
|
|
97
115
|
properties[key] = {"type": "string"}
|
|
98
116
|
else:
|
|
99
|
-
properties[key] = {"type":
|
|
117
|
+
properties[key] = {"type": type_or_obj}
|
|
100
118
|
|
|
101
119
|
return {"type": "object", "properties": properties}
|
|
102
120
|
|
|
103
121
|
|
|
122
|
+
def extract_trpc_get_input(path, query_string):
|
|
123
|
+
"""
|
|
124
|
+
tRPC GET requests send their input as a URL-encoded JSON query parameter
|
|
125
|
+
called 'input'. This function extracts and parses it.
|
|
126
|
+
Only activates for tRPC paths with an input param and empty body.
|
|
127
|
+
Safe for all non-tRPC endpoints — returns None if not applicable.
|
|
128
|
+
"""
|
|
129
|
+
if "/trpc/" not in path:
|
|
130
|
+
return None
|
|
131
|
+
if not query_string or "input=" not in query_string:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
from urllib.parse import parse_qs, unquote
|
|
136
|
+
params = parse_qs(query_string)
|
|
137
|
+
input_param = params.get("input", [None])[0]
|
|
138
|
+
if not input_param:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
decoded = json.loads(unquote(input_param))
|
|
142
|
+
if not isinstance(decoded, dict):
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
keys = list(decoded.keys())
|
|
146
|
+
|
|
147
|
+
# Format 1 — batched: { "0": { "json": {...} }, "1": { "json": {...} } }
|
|
148
|
+
is_batch = len(keys) > 0 and all(k.isdigit() for k in keys)
|
|
149
|
+
if is_batch:
|
|
150
|
+
merged = {}
|
|
151
|
+
for k in keys:
|
|
152
|
+
entry = decoded[k]
|
|
153
|
+
if isinstance(entry, dict):
|
|
154
|
+
# Unwrap { json: {...} } envelope if present
|
|
155
|
+
unwrapped = entry.get("json", entry) if isinstance(entry.get("json"), dict) else entry
|
|
156
|
+
merged.update(unwrapped)
|
|
157
|
+
return merged if merged else None
|
|
158
|
+
|
|
159
|
+
# Format 2 — single with superjson: { "json": {...}, "meta": {...} }
|
|
160
|
+
if "json" in decoded and isinstance(decoded["json"], dict):
|
|
161
|
+
return decoded["json"]
|
|
162
|
+
|
|
163
|
+
# Format 3 — already unwrapped: { "group": {...}, "limit": 10 }
|
|
164
|
+
if "meta" not in decoded and "json" not in decoded:
|
|
165
|
+
return decoded
|
|
166
|
+
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
104
173
|
def report_endpoint(client, method, path, body_structure, options):
|
|
105
174
|
"""
|
|
106
175
|
Report a newly discovered endpoint to the platform.
|
|
@@ -159,6 +228,12 @@ def attach_fastapi_interceptor(app, client, options):
|
|
|
159
228
|
request._receive = receive
|
|
160
229
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
161
230
|
body_structure = build_body_structure(body_data)
|
|
231
|
+
# tRPC GET: input is in URL query param, not body
|
|
232
|
+
if not body_structure:
|
|
233
|
+
query_string = request.url.query
|
|
234
|
+
trpc_body = extract_trpc_get_input(path, query_string)
|
|
235
|
+
if trpc_body:
|
|
236
|
+
body_structure = build_body_structure(trpc_body)
|
|
162
237
|
except Exception:
|
|
163
238
|
body_structure = None
|
|
164
239
|
|
|
@@ -191,6 +266,11 @@ def attach_flask_interceptor(app, client, options):
|
|
|
191
266
|
|
|
192
267
|
try:
|
|
193
268
|
body_structure = build_body_structure(flask_request.get_json(silent=True))
|
|
269
|
+
# tRPC GET: input is in URL query param, not body
|
|
270
|
+
if not body_structure:
|
|
271
|
+
trpc_body = extract_trpc_get_input(path, flask_request.query_string.decode("utf-8"))
|
|
272
|
+
if trpc_body:
|
|
273
|
+
body_structure = build_body_structure(trpc_body)
|
|
194
274
|
except Exception:
|
|
195
275
|
body_structure = None
|
|
196
276
|
|
|
@@ -222,6 +302,11 @@ class BotVersionDjangoMiddleware:
|
|
|
222
302
|
try:
|
|
223
303
|
body_data = json.loads(request.body) if request.body else None
|
|
224
304
|
body_structure = build_body_structure(body_data)
|
|
305
|
+
# tRPC GET: input is in URL query param, not body
|
|
306
|
+
if not body_structure:
|
|
307
|
+
trpc_body = extract_trpc_get_input(path, request.META.get("QUERY_STRING", ""))
|
|
308
|
+
if trpc_body:
|
|
309
|
+
body_structure = build_body_structure(trpc_body)
|
|
225
310
|
except Exception:
|
|
226
311
|
body_structure = None
|
|
227
312
|
|
|
@@ -281,6 +366,12 @@ def attach_starlette_interceptor(app, client, options):
|
|
|
281
366
|
body_bytes = await request.body()
|
|
282
367
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
283
368
|
body_structure = build_body_structure(body_data)
|
|
369
|
+
# tRPC GET: input is in URL query param, not body
|
|
370
|
+
if not body_structure:
|
|
371
|
+
query_string = request.url.query
|
|
372
|
+
trpc_body = extract_trpc_get_input(path, query_string)
|
|
373
|
+
if trpc_body:
|
|
374
|
+
body_structure = build_body_structure(trpc_body)
|
|
284
375
|
except Exception:
|
|
285
376
|
body_structure = None
|
|
286
377
|
|
|
@@ -312,6 +403,11 @@ def attach_sanic_interceptor(app, client, options):
|
|
|
312
403
|
try:
|
|
313
404
|
body_data = request.json if request.body else None
|
|
314
405
|
body_structure = build_body_structure(body_data)
|
|
406
|
+
# tRPC GET: input is in URL query param, not body
|
|
407
|
+
if not body_structure:
|
|
408
|
+
trpc_body = extract_trpc_get_input(path, request.query_string)
|
|
409
|
+
if trpc_body:
|
|
410
|
+
body_structure = build_body_structure(trpc_body)
|
|
315
411
|
except Exception:
|
|
316
412
|
body_structure = None
|
|
317
413
|
|
|
@@ -341,6 +437,11 @@ def attach_falcon_interceptor(app, client, options):
|
|
|
341
437
|
body_bytes = req.bounded_stream.read()
|
|
342
438
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
343
439
|
body_structure = build_body_structure(body_data)
|
|
440
|
+
# tRPC GET: input is in URL query param, not body
|
|
441
|
+
if not body_structure:
|
|
442
|
+
trpc_body = extract_trpc_get_input(path, req.query_string)
|
|
443
|
+
if trpc_body:
|
|
444
|
+
body_structure = build_body_structure(trpc_body)
|
|
344
445
|
# Put body back so the actual handler can still read it
|
|
345
446
|
import io
|
|
346
447
|
req.bounded_stream = io.BytesIO(body_bytes)
|
|
@@ -373,6 +474,11 @@ def attach_bottle_interceptor(app, client, options):
|
|
|
373
474
|
try:
|
|
374
475
|
body_data = bottle_request.json
|
|
375
476
|
body_structure = build_body_structure(body_data)
|
|
477
|
+
# tRPC GET: input is in URL query param, not body
|
|
478
|
+
if not body_structure:
|
|
479
|
+
trpc_body = extract_trpc_get_input(path, bottle_request.query_string)
|
|
480
|
+
if trpc_body:
|
|
481
|
+
body_structure = build_body_structure(trpc_body)
|
|
376
482
|
except Exception:
|
|
377
483
|
body_structure = None
|
|
378
484
|
|
|
@@ -405,6 +511,11 @@ def attach_aiohttp_interceptor(app, client, options):
|
|
|
405
511
|
body_bytes = await request.read()
|
|
406
512
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
407
513
|
body_structure = build_body_structure(body_data)
|
|
514
|
+
# tRPC GET: input is in URL query param, not body
|
|
515
|
+
if not body_structure:
|
|
516
|
+
trpc_body = extract_trpc_get_input(path, request.rel_url.query_string)
|
|
517
|
+
if trpc_body:
|
|
518
|
+
body_structure = build_body_structure(trpc_body)
|
|
408
519
|
except Exception:
|
|
409
520
|
body_structure = None
|
|
410
521
|
|
|
@@ -450,6 +561,11 @@ def attach_tornado_interceptor(app, client, options):
|
|
|
450
561
|
body_bytes = self.request.body
|
|
451
562
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
452
563
|
body_structure = build_body_structure(body_data)
|
|
564
|
+
# tRPC GET: input is in URL query param, not body
|
|
565
|
+
if not body_structure:
|
|
566
|
+
trpc_body = extract_trpc_get_input(path, self.request.query)
|
|
567
|
+
if trpc_body:
|
|
568
|
+
body_structure = build_body_structure(trpc_body)
|
|
453
569
|
except Exception:
|
|
454
570
|
body_structure = None
|
|
455
571
|
|
|
@@ -487,6 +603,11 @@ def botversion_pyramid_tween_factory(handler, registry):
|
|
|
487
603
|
try:
|
|
488
604
|
body_data = request.json_body if request.content_length else None
|
|
489
605
|
body_structure = build_body_structure(body_data)
|
|
606
|
+
# tRPC GET: input is in URL query param, not body
|
|
607
|
+
if not body_structure:
|
|
608
|
+
trpc_body = extract_trpc_get_input(path, request.query_string)
|
|
609
|
+
if trpc_body:
|
|
610
|
+
body_structure = build_body_structure(trpc_body)
|
|
490
611
|
except Exception:
|
|
491
612
|
body_structure = None
|
|
492
613
|
|
|
@@ -533,6 +654,12 @@ def attach_cherrypy_interceptor(app, client, options):
|
|
|
533
654
|
body_bytes = request.body.read() if request.body else None
|
|
534
655
|
body_data = _json.loads(body_bytes) if body_bytes else None
|
|
535
656
|
body_structure = build_body_structure(body_data)
|
|
657
|
+
# tRPC GET: input is in URL query param, not body
|
|
658
|
+
if not body_structure:
|
|
659
|
+
query_string = request.query_string if hasattr(request, "query_string") else ""
|
|
660
|
+
trpc_body = extract_trpc_get_input(path, query_string)
|
|
661
|
+
if trpc_body:
|
|
662
|
+
body_structure = build_body_structure(trpc_body)
|
|
536
663
|
# Put body back so actual handler can still read it
|
|
537
664
|
import io
|
|
538
665
|
request.body = io.BytesIO(body_bytes or b"")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "botversion-sdk"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.7"
|
|
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"
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="botversion-sdk",
|
|
5
|
-
version="1.1.
|
|
5
|
+
version="1.1.7",
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|